diff --git a/.bettercodehub.yml b/.bettercodehub.yml index 349f7675..d7be5169 100644 --- a/.bettercodehub.yml +++ b/.bettercodehub.yml @@ -1,3 +1,3 @@ -component_depth: 8 +component_depth: 10 languages: - kotlin diff --git a/.travis.yml b/.travis.yml index 514e37c0..ee3e6f3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ cache: branches: only: - develop - - 0.17.4 + - 0.18.0 android: licenses: @@ -48,20 +48,15 @@ before_script: script: - ./gradlew dependencies --stacktrace --daemon - fossa --no-ansi || true - #- ./gradlew lintPlayRelease -x fabricGenerateResourcesPlayRelease --stacktrace --daemon - - ./gradlew -Pcoverage testPlayDebugUnitTest -x fabricGenerateResourcesPlay --stacktrace --daemon + - ./gradlew -Pcoverage testPlayDebugUnitTest --stacktrace --daemon - ./gradlew -Pcoverage createFdroidDebugCoverageReport --stacktrace --daemon - ./gradlew -Pcoverage jacocoTestReport --stacktrace --daemon - - if [ -z ${SONAR_HOST+x} ]; then echo "sonar scan skipped"; else - git fetch --unshallow; - ./gradlew sonarqube -x test -x lint -x fabricGenerateResourcesPlayRelease -x fabricGenerateResourcesFdroidRelease -Dsonar.host.url=$SONAR_HOST -Dsonar.organization=$SONAR_ORG -Dsonar.login=$SONAR_KEY -Dsonar.branch.name=${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} --stacktrace --daemon; - fi - | if [ $TRAVIS_TAG ]; then gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg; - ./gradlew publishPlayRelease -PenableCrashlytics --stacktrace; + ./gradlew publishPlayRelease -PenableFirebase --stacktrace; fi after_success: diff --git a/app/build.gradle b/app/build.gradle index 45d20f96..c1e8b581 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,8 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' -apply plugin: 'kotlin-android-extensions' -apply plugin: 'io.fabric' +apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.github.triplet.play' apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply from: 'jacoco.gradle' @@ -18,14 +17,13 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 17 targetSdkVersion 29 - versionCode 57 - versionName "0.17.4" + versionCode 59 + versionName "0.18.0" multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true manifestPlaceholders = [ - fabric_api_key : System.getenv("FABRIC_API_KEY") ?: "null", - crashlytics_enabled: project.hasProperty("enableCrashlytics") + firebase_enabled: project.hasProperty("enableFirebase") ] javaCompileOptions { annotationProcessorOptions { @@ -52,18 +50,16 @@ android { buildTypes { release { - buildConfigField "boolean", "CRASHLYTICS_ENABLED", "true" minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } debug { - buildConfigField "boolean", "CRASHLYTICS_ENABLED", project.hasProperty("enableCrashlytics") ? "true" : "false" applicationIdSuffix ".dev" versionNameSuffix "-dev" testCoverageEnabled = project.hasProperty('coverage') - ext.enableCrashlytics = project.hasProperty("enableCrashlytics") + ext.enableCrashlytics = project.hasProperty("enableFirebase") } } @@ -75,11 +71,14 @@ android { } fdroid { - buildConfigField "boolean", "CRASHLYTICS_ENABLED", "false" dimension "platform" } } + viewBinding { + enabled = true + } + lintOptions { disable 'HardwareIds' } @@ -103,10 +102,6 @@ android { } } -androidExtensions { - experimental = true -} - play { serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" serviceAccountCredentials = file('key.p12') @@ -118,7 +113,6 @@ ext { work_manager = "2.3.4" room = "2.2.5" dagger = "2.27" - // don't update https://github.com/ChuckerTeam/chucker/issues/242 chucker = "3.2.0" mockk = "1.9.2" } @@ -128,12 +122,12 @@ configurations.all { } 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 "androidx.core:core-ktx:1.2.0" implementation "androidx.activity:activity-ktx:1.1.0" - implementation "androidx.appcompat:appcompat:1.2.0-beta01" + implementation "androidx.appcompat:appcompat:1.2.0-rc01" implementation "androidx.appcompat:appcompat-resources:1.1.0" implementation "androidx.fragment:fragment-ktx:1.2.4" implementation "androidx.annotation:annotation:1.1.0" @@ -167,30 +161,29 @@ dependencies { implementation "com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2" kapt "com.squareup.inject:assisted-inject-processor-dagger2:0.5.2" - implementation "eu.davidea:flexible-adapter:5.1.0" - implementation "eu.davidea:flexible-adapter-ui:1.0.0" implementation "com.aurelhubert:ahbottomnavigation:2.3.4" implementation "com.ncapdevi:frag-nav:3.3.0" - implementation "com.github.YarikSOffice:lingver:1.2.1" + implementation "com.github.YarikSOffice:lingver:1.2.2" - implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.6" + implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.8" implementation "io.reactivex.rxjava2:rxandroid:2.1.1" implementation "io.reactivex.rxjava2:rxjava:2.2.19" implementation "com.google.code.gson:gson:2.8.6" - implementation "com.jakewharton.threetenabp:threetenabp:1.2.3" + implementation "com.jakewharton.threetenabp:threetenabp:1.2.4" implementation "com.jakewharton.timber:timber:4.7.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation "fr.bipi.treessence:treessence:0.3.2" implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation 'com.wdullaer:materialdatetimepicker:4.2.3' - implementation "io.coil-kt:coil:0.9.5" + implementation "io.coil-kt:coil:0.11.0" + implementation "io.github.wulkanowy:AppKillerManager:3.0.0" - playImplementation 'com.google.firebase:firebase-analytics:17.3.0' - playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.5' - playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.5" - playImplementation "com.google.firebase:firebase-messaging:20.1.0" - playImplementation "com.crashlytics.sdk.android:crashlytics:2.10.1" + playImplementation 'com.google.firebase:firebase-analytics:17.4.1' + playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.6' + playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.6" + playImplementation 'com.google.firebase:firebase-messaging:20.1.7' + playImplementation 'com.google.firebase:firebase-crashlytics:17.0.0' playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" @@ -200,7 +193,7 @@ dependencies { testImplementation "junit:junit:4.13" testImplementation "io.mockk:mockk:$mockk" - testImplementation "org.threeten:threetenbp:1.4.3" + testImplementation "org.threeten:threetenbp:1.4.4" testImplementation "org.mockito:mockito-inline:3.3.3" androidTestImplementation "androidx.test:core:1.2.0" diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeLocalTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeLocalTest.kt index 7233c306..eb1a5548 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeLocalTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeLocalTest.kt @@ -24,7 +24,7 @@ class GradeLocalTest { fun createDb() { testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java) .build() - gradeLocal = GradeLocal(testDb.gradeDao) + gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao) } @After @@ -43,7 +43,7 @@ class GradeLocalTest { val semester = Semester(1, 2, "", 2019, 2, 1, now(), now(), 1, 1) val grades = gradeLocal - .getGrades(semester) + .getGradesDetails(semester) .blockingGet() assertEquals(2, grades.size) diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeRepositoryTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeRepositoryTest.kt index f3c6b7a1..cdd51477 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeRepositoryTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/grade/GradeRepositoryTest.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.TestInternetObservingStrategy import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.pojo.Grade import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -52,7 +53,7 @@ class GradeRepositoryTest { fun initApi() { MockKAnnotations.init(this) testDb = Room.inMemoryDatabaseBuilder(getApplicationContext(), AppDatabase::class.java).build() - gradeLocal = GradeLocal(testDb.gradeDao) + gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao) gradeRemote = GradeRemote(mockSdk) every { studentMock.registrationDate } returns LocalDateTime.of(2019, 2, 27, 12, 0) @@ -75,10 +76,10 @@ class GradeRepositoryTest { createGradeApi(5, 4.0, of(2019, 2, 26), "przed zalogowanie w aplikacji"), createGradeApi(5, 4.0, of(2019, 2, 27), "Ocena z dnia logowania"), createGradeApi(5, 4.0, of(2019, 2, 28), "Ocena jeszcze nowsza") - )) + ) to emptyList()) val grades = GradeRepository(settings, gradeLocal, gradeRemote) - .getGrades(studentMock, semesterMock, true).blockingGet().sortedByDescending { it.date } + .getGrades(studentMock, semesterMock, true).blockingGet().first.sortedByDescending { it.date } assertFalse { grades[0].isRead } assertFalse { grades[1].isRead } @@ -99,10 +100,10 @@ class GradeRepositoryTest { createGradeApi(4, 3.0, of(2019, 2, 26), "starszą niż ostatnia lokalnie"), createGradeApi(3, 4.0, of(2019, 2, 27), "Ta jest z tego samego dnia co ostatnia lokalnie"), createGradeApi(2, 5.0, of(2019, 2, 28), "Ta jest już w ogóle nowa") - )) + ) to emptyList()) val grades = GradeRepository(settings, gradeLocal, gradeRemote) - .getGrades(studentMock, semesterMock, true).blockingGet().sortedByDescending { it.date } + .getGrades(studentMock, semesterMock, true).blockingGet().first.sortedByDescending { it.date } assertFalse { grades[0].isRead } assertFalse { grades[1].isRead } @@ -121,12 +122,12 @@ class GradeRepositoryTest { every { mockSdk.getGrades(1) } returns Single.just(listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") - )) + ) to emptyList()) val grades = GradeRepository(settings, gradeLocal, gradeRemote) .getGrades(studentMock, semesterMock, true).blockingGet() - assertEquals(2, grades.size) + assertEquals(2, grades.first.size) } @Test @@ -140,12 +141,12 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") - )) + ) to emptyList()) val grades = GradeRepository(settings, gradeLocal, gradeRemote) .getGrades(studentMock, semesterMock, true).blockingGet() - assertEquals(3, grades.size) + assertEquals(3, grades.first.size) } @Test @@ -156,12 +157,12 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") - )) + ) to emptyList()) val grades = GradeRepository(settings, gradeLocal, gradeRemote) .getGrades(studentMock, semesterMock, true).blockingGet() - assertEquals(3, grades.size) + assertEquals(3, grades.first.size) } @Test @@ -171,11 +172,11 @@ class GradeRepositoryTest { createGradeLocal(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") )) - every { mockSdk.getGrades(1) } returns Single.just(listOf()) + every { mockSdk.getGrades(1) } returns Single.just(emptyList() to emptyList()) val grades = GradeRepository(settings, gradeLocal, gradeRemote) .getGrades(studentMock, semesterMock, true).blockingGet() - assertEquals(0, grades.size) + assertEquals(0, grades.first.size) } } diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepositoryTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepositoryTest.kt index fdf193a2..75f2f0b8 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepositoryTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepositoryTest.kt @@ -8,12 +8,15 @@ import androidx.test.filters.SdkSuppress import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.TestInternetObservingStrategy import io.github.wulkanowy.data.repositories.getStudent +import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.sdk.Sdk import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import io.reactivex.Single import org.junit.After import org.junit.Before @@ -34,11 +37,17 @@ class TimetableRepositoryTest { .strategy(TestInternetObservingStrategy()) .build() + @MockK + private lateinit var studentMock: Student + private val student = getStudent() @MockK private lateinit var semesterMock: Semester + @MockK + private lateinit var timetableNotificationSchedulerHelper: TimetableNotificationSchedulerHelper + private lateinit var timetableRemote: TimetableRemote private lateinit var timetableLocal: TimetableLocal @@ -52,10 +61,17 @@ class TimetableRepositoryTest { timetableLocal = TimetableLocal(testDb.timetableDao) timetableRemote = TimetableRemote(mockSdk) + every { timetableNotificationSchedulerHelper.scheduleNotifications(any(), any()) } returns mockk() + every { timetableNotificationSchedulerHelper.cancelScheduled(any(), any()) } returns mockk() + + every { studentMock.studentId } returns 1 + every { studentMock.studentName } returns "Jan Kowalski" + every { semesterMock.studentId } returns 1 every { semesterMock.diaryId } returns 2 every { semesterMock.schoolYear } returns 2019 every { semesterMock.semesterId } returns 1 + every { mockSdk.switchDiary(any(), any()) } returns mockSdk } @@ -80,7 +96,7 @@ class TimetableRepositoryTest { createTimetableRemote(of(2019, 3, 5, 10, 30), 4, "", "W-F") )) - val lessons = TimetableRepository(settings, timetableLocal, timetableRemote) + val lessons = TimetableRepository(settings, timetableLocal, timetableRemote, timetableNotificationSchedulerHelper) .getTimetable(student, semesterMock, LocalDate.of(2019, 3, 5), LocalDate.of(2019, 3, 5), true) .blockingGet() @@ -126,7 +142,7 @@ class TimetableRepositoryTest { createTimetableRemote(of(2019, 12, 25, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true) )) - val lessons = TimetableRepository(settings, timetableLocal, timetableRemote) + val lessons = TimetableRepository(settings, timetableLocal, timetableRemote, timetableNotificationSchedulerHelper) .getTimetable(student, semesterMock, LocalDate.of(2019, 12, 23), LocalDate.of(2019, 12, 25), true) .blockingGet() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 42c75475..802cf1ad 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,8 @@ android:supportsRtl="false" android:theme="@style/WulkanowyTheme" android:usesCleartextTraffic="true" - tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> + tools:ignore="GoogleAppIndexingWarning,UnusedAttribute" + tools:replace="android:supportsRtl,android:allowBackup"> + android:theme="@style/WulkanowyTheme.NoActionBar" + android:windowSoftInputMode="adjustPan" /> + + + + + + + android:name="firebase_analytics_collection_enabled" + android:value="${firebase_enabled}" /> + + + + android:value="${firebase_enabled}" /> + + + + > { - return Single.fromCallable> { + fun getAppCreators(): Single> { + return Single.fromCallable> { Gson().fromJson( assets.open("contributors.json").bufferedReader().use { it.readText() }, - Array::class.java + Array::class.java ).toList() } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeLocal.kt index 4983a474..944ed34a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeLocal.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeLocal.kt @@ -1,14 +1,19 @@ package io.github.wulkanowy.data.repositories.grade import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeSummaryDao import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.reactivex.Maybe import javax.inject.Inject import javax.inject.Singleton @Singleton -class GradeLocal @Inject constructor(private val gradeDb: GradeDao) { +class GradeLocal @Inject constructor( + private val gradeDb: GradeDao, + private val gradeSummaryDb: GradeSummaryDao +) { fun saveGrades(grades: List) { gradeDb.insertAll(grades) @@ -22,7 +27,19 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) { gradeDb.updateAll(grades) } - fun getGrades(semester: Semester): Maybe> { + fun getGradesDetails(semester: Semester): Maybe> { return gradeDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() } } + + fun saveGradesSummary(gradesSummary: List) { + gradeSummaryDb.insertAll(gradesSummary) + } + + fun deleteGradesSummary(gradesSummary: List) { + gradeSummaryDb.deleteAll(gradesSummary) + } + + fun getGradesSummary(semester: Semester): Maybe> { + return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() } + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeRemote.kt index 6f19095b..abb2f98c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeRemote.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeRemote.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories.grade import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.sdk.Sdk @@ -12,11 +13,11 @@ import javax.inject.Singleton @Singleton class GradeRemote @Inject constructor(private val sdk: Sdk) { - fun getGrades(student: Student, semester: Semester): Single> { + fun getGrades(student: Student, semester: Semester): Single, List>> { return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getGrades(semester.semesterId) - .map { grades -> - grades.map { + .map { (details, summary) -> + details.map { Grade( studentId = semester.studentId, semesterId = semester.semesterId, @@ -33,6 +34,19 @@ class GradeRemote @Inject constructor(private val sdk: Sdk) { date = it.date, teacher = it.teacher ) + } to summary.map { + GradeSummary( + semesterId = semester.semesterId, + studentId = semester.studentId, + position = 0, + subject = it.name, + predictedGrade = it.predicted, + finalGrade = it.final, + pointsSum = it.pointsSum, + proposedPoints = it.proposedPoints, + finalPoints = it.finalPoints, + average = it.average + ) } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeRepository.kt index 351ebc39..e28350a5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/grade/GradeRepository.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories.grade import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.utils.uniqueSubtract @@ -19,34 +20,47 @@ class GradeRepository @Inject constructor( private val remote: GradeRemote ) { - fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single> { - return local.getGrades(semester).filter { !forceRefresh } - .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) - .flatMap { - if (it) remote.getGrades(student, semester) - else Single.error(UnknownHostException()) - }.flatMap { new -> - local.getGrades(semester).toSingle(emptyList()) - .doOnSuccess { old -> - val notifyBreakDate = old.maxBy { it.date }?.date ?: student.registrationDate.toLocalDate() - local.deleteGrades(old.uniqueSubtract(new)) - local.saveGrades(new.uniqueSubtract(old) - .onEach { - if (it.date >= notifyBreakDate) it.apply { - isRead = false - if (notify) isNotified = false - } - }) - } - }.flatMap { local.getGrades(semester).toSingle(emptyList()) }) + fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single, List>> { + return local.getGradesDetails(semester).flatMap { details -> + local.getGradesSummary(semester).map { summary -> details to summary } + }.filter { !forceRefresh } + .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap { + if (it) remote.getGrades(student, semester) + else Single.error(UnknownHostException()) + }.flatMap { (newDetails, newSummary) -> + local.getGradesDetails(semester).toSingle(emptyList()) + .doOnSuccess { old -> + val notifyBreakDate = old.maxBy { it.date }?.date ?: student.registrationDate.toLocalDate() + local.deleteGrades(old.uniqueSubtract(newDetails)) + local.saveGrades(newDetails.uniqueSubtract(old) + .onEach { + if (it.date >= notifyBreakDate) it.apply { + isRead = false + if (notify) isNotified = false + } + }) + }.flatMap { + local.getGradesSummary(semester).toSingle(emptyList()) + .doOnSuccess { old -> + local.deleteGradesSummary(old.uniqueSubtract(newSummary)) + local.saveGradesSummary(newSummary.uniqueSubtract(old)) + } + } + }.flatMap { + local.getGradesDetails(semester).toSingle(emptyList()).flatMap { details -> + local.getGradesSummary(semester).toSingle(emptyList()).map { summary -> + details to summary + } + } + }) } fun getUnreadGrades(semester: Semester): Single> { - 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> { - 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 { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryLocal.kt deleted file mode 100644 index e74641d3..00000000 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryLocal.kt +++ /dev/null @@ -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) { - gradeSummaryDb.insertAll(gradesSummary) - } - - fun deleteGradesSummary(gradesSummary: List) { - gradeSummaryDb.deleteAll(gradesSummary) - } - - fun getGradesSummary(semester: Semester): Maybe> { - return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryRemote.kt deleted file mode 100644 index 1b09974d..00000000 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryRemote.kt +++ /dev/null @@ -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> { - 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 - ) - } - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryRepository.kt deleted file mode 100644 index f1d7a6c1..00000000 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryRepository.kt +++ /dev/null @@ -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> { - 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()) }) - } -} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/preferences/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/preferences/PreferencesRepository.kt index 523caf6c..6b13563a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/preferences/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/preferences/PreferencesRepository.kt @@ -26,6 +26,9 @@ class PreferencesRepository @Inject constructor( val isGradeExpandable: Boolean get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade) + val showAllSubjectsOnStatisticsList: Boolean + get() = getBoolean(R.string.pref_key_grade_statistics_list, R.bool.pref_default_grade_statistics_list) + val appThemeKey = context.getString(R.string.pref_key_app_theme) val appTheme: String get() = getString(appThemeKey, R.string.pref_default_app_theme) @@ -52,6 +55,10 @@ class PreferencesRepository @Inject constructor( val isNotificationsEnable: Boolean get() = getBoolean(R.string.pref_key_notifications_enable, R.bool.pref_default_notifications_enable) + val isUpcomingLessonsNotificationsEnableKey = context.getString(R.string.pref_key_notifications_upcoming_lessons_enable) + val isUpcomingLessonsNotificationsEnable: Boolean + get() = getBoolean(isUpcomingLessonsNotificationsEnableKey, R.bool.pref_default_notification_upcoming_lessons_enable) + val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug) val isDebugNotificationEnable: Boolean get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug) @@ -68,6 +75,9 @@ class PreferencesRepository @Inject constructor( val showWholeClassPlan: String get() = getString(R.string.pref_key_timetable_show_whole_class, R.string.pref_default_timetable_show_whole_class) + val showTimetableTimers: Boolean + get() = getBoolean(R.string.pref_key_timetable_show_timers, R.bool.pref_default_timetable_show_timers) + private fun getString(id: Int, default: Int) = getString(context.getString(id), default) private fun getString(id: String, default: Int) = sharedPref.getString(id, context.getString(default)) ?: context.getString(default) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepository.kt index 42812b30..082b03cf 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/timetable/TimetableRepository.kt @@ -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.Student 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.monday import io.github.wulkanowy.utils.uniqueSubtract @@ -18,7 +19,8 @@ import javax.inject.Singleton class TimetableRepository @Inject constructor( private val settings: InternetObservingSettings, private val local: TimetableLocal, - private val remote: TimetableRemote + private val remote: TimetableRemote, + private val schedulerHelper: TimetableNotificationSchedulerHelper ) { fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single> { @@ -31,8 +33,8 @@ class TimetableRepository @Inject constructor( local.getTimetable(semester, monday, friday) .toSingle(emptyList()) .doOnSuccess { old -> - local.deleteTimetable(old.uniqueSubtract(new)) - local.saveTimetable(new.uniqueSubtract(old).map { item -> + local.deleteTimetable(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) }) + local.saveTimetable(new.uniqueSubtract(old).also { schedulerHelper.scheduleNotifications(it, student) }.map { item -> item.also { new -> old.singleOrNull { new.start == it.start }?.let { old -> return@map new.copy( @@ -45,7 +47,7 @@ class TimetableRepository @Inject constructor( } }.flatMap { 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) } } } } } diff --git a/app/src/main/java/io/github/wulkanowy/di/AppModule.kt b/app/src/main/java/io/github/wulkanowy/di/AppModule.kt index 4f568385..db5ff59b 100644 --- a/app/src/main/java/io/github/wulkanowy/di/AppModule.kt +++ b/app/src/main/java/io/github/wulkanowy/di/AppModule.kt @@ -5,8 +5,6 @@ import android.content.Context import com.yariksoffice.lingver.Lingver import dagger.Module import dagger.Provides -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.WulkanowyApp import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.SchedulersProvider @@ -23,9 +21,6 @@ internal class AppModule { @Provides fun provideSchedulersProvider() = SchedulersProvider() - @Provides - fun provideFlexibleAdapter() = FlexibleAdapter>(null, null, true) - @Singleton @Provides fun provideAppWidgetManager(context: Context): AppWidgetManager = AppWidgetManager.getInstance(context) diff --git a/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt b/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt index ba8c78d3..1b462964 100644 --- a/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt +++ b/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt @@ -4,6 +4,7 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import io.github.wulkanowy.di.scopes.PerActivity import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginModule import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity @@ -48,4 +49,7 @@ internal abstract class BindingModule { @ContributesAndroidInjector abstract fun bindLuckyNumberWidgetProvider(): LuckyNumberWidgetProvider + + @ContributesAndroidInjector + abstract fun bindTimetableNotificationReceiver(): TimetableNotificationReceiver } diff --git a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt index 7240a50b..b87f0e68 100644 --- a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt +++ b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt @@ -1,7 +1,9 @@ package io.github.wulkanowy.services +import android.app.AlarmManager import android.content.Context import androidx.core.app.NotificationManagerCompat +import androidx.core.content.getSystemService import androidx.work.WorkManager import com.squareup.inject.assisted.dagger2.AssistedModule import dagger.Binds @@ -15,13 +17,13 @@ import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel +import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel import io.github.wulkanowy.services.sync.channels.PushChannel import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork import io.github.wulkanowy.services.sync.works.AttendanceWork import io.github.wulkanowy.services.sync.works.CompletedLessonWork import io.github.wulkanowy.services.sync.works.ExamWork import io.github.wulkanowy.services.sync.works.GradeStatisticsWork -import io.github.wulkanowy.services.sync.works.GradeSummaryWork import io.github.wulkanowy.services.sync.works.GradeWork import io.github.wulkanowy.services.sync.works.HomeworkWork import io.github.wulkanowy.services.sync.works.LuckyNumberWork @@ -47,6 +49,10 @@ abstract class ServicesModule { @Singleton @Provides fun provideNotificationManager(context: Context) = NotificationManagerCompat.from(context) + + @Singleton + @Provides + fun provideAlarmManager(context: Context): AlarmManager = context.getSystemService()!! } @ContributesAndroidInjector @@ -64,10 +70,6 @@ abstract class ServicesModule { @IntoSet abstract fun provideAttendanceWork(work: AttendanceWork): Work - @Binds - @IntoSet - abstract fun provideGradeSummaryWork(work: GradeSummaryWork): Work - @Binds @IntoSet abstract fun provideExamWork(work: ExamWork): Work @@ -131,4 +133,8 @@ abstract class ServicesModule { @Binds @IntoSet abstract fun providePushChannel(channel: PushChannel): Channel + + @Binds + @IntoSet + abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel } diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt new file mode 100644 index 00000000..0130f467 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -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() + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt new file mode 100644 index 00000000..5374c476 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -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, lesson: Timetable): LocalDateTime { + return day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30) + } + + fun cancelScheduled(lessons: List, 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, 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, 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") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index dda2abe0..965ed0ad 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt @@ -42,7 +42,7 @@ class SyncManager @Inject constructor( init { if (now().isHolidays) stopSyncWorker() - if (SDK_INT > O) { + if (SDK_INT >= O) { channels.forEach { it.create() } notificationManager.deleteNotificationChannel("new_entries_channel") } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/UpcomingLessonsChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/UpcomingLessonsChannel.kt new file mode 100644 index 00000000..a292c8b5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/UpcomingLessonsChannel.kt @@ -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) + } + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeSummaryWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeSummaryWork.kt deleted file mode 100644 index 4c8e955d..00000000 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeSummaryWork.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt index 7559d289..6e90826a 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt @@ -58,4 +58,3 @@ class GradeWork @Inject constructor( ) } } - diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index ee74832f..f20fb22f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -11,6 +11,7 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import dagger.android.AndroidInjection @@ -20,10 +21,13 @@ import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.utils.FragmentLifecycleLogger import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.lifecycleAwareVariable import javax.inject.Inject -abstract class BaseActivity> : AppCompatActivity(), BaseView, - HasAndroidInjector { +abstract class BaseActivity, VB : ViewBinding> : + AppCompatActivity(), BaseView, HasAndroidInjector { + + protected var binding: VB by lifecycleAwareVariable() @Inject lateinit var androidInjector: DispatchingAndroidInjector diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index fdc46371..0279dccf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -1,9 +1,13 @@ package io.github.wulkanowy.ui.base import android.widget.Toast +import androidx.viewbinding.ViewBinding import dagger.android.support.DaggerAppCompatDialogFragment +import io.github.wulkanowy.utils.lifecycleAwareVariable -abstract class BaseDialogFragment : DaggerAppCompatDialogFragment(), BaseView { +abstract class BaseDialogFragment : DaggerAppCompatDialogFragment(), BaseView { + + protected var binding: VB by lifecycleAwareVariable() override fun showError(text: String, error: Throwable) { showMessage(text) @@ -14,11 +18,11 @@ abstract class BaseDialogFragment : DaggerAppCompatDialogFragment(), BaseView { } override fun showExpiredDialog() { - (activity as? BaseActivity<*>)?.showExpiredDialog() + (activity as? BaseActivity<*, *>)?.showExpiredDialog() } override fun openClearLoginView() { - (activity as? BaseActivity<*>)?.openClearLoginView() + (activity as? BaseActivity<*, *>)?.openClearLoginView() } override fun showErrorDetailsDialog(error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseExpandableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseExpandableAdapter.kt new file mode 100644 index 00000000..eee4625c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseExpandableAdapter.kt @@ -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 : RecyclerView.Adapter() { + + 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) + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index 2f5878d0..83f78765 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -1,12 +1,18 @@ package io.github.wulkanowy.ui.base import android.view.View +import androidx.annotation.LayoutRes +import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import dagger.android.support.DaggerFragment import io.github.wulkanowy.R +import io.github.wulkanowy.utils.lifecycleAwareVariable -abstract class BaseFragment : DaggerFragment(), BaseView { +abstract class BaseFragment(@LayoutRes layoutId: Int) : DaggerFragment(layoutId), + BaseView { + + protected var binding: VB by lifecycleAwareVariable() protected var messageContainer: View? = null @@ -16,7 +22,7 @@ abstract class BaseFragment : DaggerFragment(), BaseView { .setAction(R.string.all_details) { if (isAdded) showErrorDetailsDialog(error) } .show() } else { - (activity as? BaseActivity<*>)?.showError(text, error) + (activity as? BaseActivity<*, *>)?.showError(text, error) } } @@ -28,15 +34,15 @@ abstract class BaseFragment : DaggerFragment(), BaseView { if (messageContainer != null) { Snackbar.make(messageContainer!!, text, LENGTH_LONG).show() } else { - (activity as? BaseActivity<*>)?.showMessage(text) + (activity as? BaseActivity<*, *>)?.showMessage(text) } } override fun showExpiredDialog() { - (activity as? BaseActivity<*>)?.showExpiredDialog() + (activity as? BaseActivity<*, *>)?.showExpiredDialog() } override fun openClearLoginView() { - (activity as? BaseActivity<*>)?.openClearLoginView() + (activity as? BaseActivity<*, *>)?.openClearLoginView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt index ae3604bf..5fd6a86a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt @@ -11,6 +11,7 @@ import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.core.content.getSystemService import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogErrorBinding import io.github.wulkanowy.sdk.exception.FeatureDisabledException import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.exception.ServiceUnavailableException @@ -18,7 +19,6 @@ import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.getString import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser -import kotlinx.android.synthetic.main.dialog_error.* import java.io.InterruptedIOException import java.io.PrintWriter import java.io.StringWriter @@ -26,7 +26,7 @@ import java.net.SocketTimeoutException import java.net.UnknownHostException import javax.inject.Inject -class ErrorDialog : BaseDialogFragment() { +class ErrorDialog : BaseDialogFragment() { private lateinit var error: Throwable @@ -52,7 +52,7 @@ class ErrorDialog : BaseDialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_error, container, false) + return DialogErrorBinding.inflate(inflater).apply { binding = this }.root } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -62,27 +62,29 @@ class ErrorDialog : BaseDialogFragment() { error.printStackTrace(PrintWriter(this)) } - errorDialogContent.text = stringWriter.toString() - with(errorDialogHorizontalScroll) { - post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } - } - errorDialogCopy.setOnClickListener { - val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString()) - activity?.getSystemService()?.setPrimaryClip(clip) + with(binding) { + errorDialogContent.text = stringWriter.toString() + with(errorDialogHorizontalScroll) { + post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } + } + errorDialogCopy.setOnClickListener { + val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString()) + activity?.getSystemService()?.setPrimaryClip(clip) - Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show() - } - errorDialogCancel.setOnClickListener { dismiss() } - errorDialogReport.setOnClickListener { openEmailClient(stringWriter.toString()) } - errorDialogMessage.text = resources.getString(error) - errorDialogReport.isEnabled = when (error) { - is UnknownHostException, - is InterruptedIOException, - is SocketTimeoutException, - is ServiceUnavailableException, - is FeatureDisabledException, - is FeatureNotAvailableException -> false - else -> true + Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show() + } + errorDialogCancel.setOnClickListener { dismiss() } + errorDialogReport.setOnClickListener { openEmailClient(stringWriter.toString()) } + errorDialogMessage.text = resources.getString(error) + errorDialogReport.isEnabled = when (error) { + is UnknownHostException, + is InterruptedIOException, + is SocketTimeoutException, + is ServiceUnavailableException, + is FeatureDisabledException, + is FeatureNotAvailableException -> false + else -> true + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/WidgetConfigureAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/WidgetConfigureAdapter.kt new file mode 100644 index 00000000..cefe6ed7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/base/WidgetConfigureAdapter.kt @@ -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() { + + var items = emptyList>() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutAdapter.kt new file mode 100644 index 00000000..35dec3b4 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutAdapter.kt @@ -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() { + + private enum class ViewType(val id: Int) { + ITEM_HEADER(1), + ITEM_ELEMENT(2) + } + + var items = emptyList>() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutFragment.kt index 5a32ac83..3828a2bc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutFragment.kt @@ -2,13 +2,10 @@ package io.github.wulkanowy.ui.modules.about import android.graphics.drawable.Drawable import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentAboutBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.about.contributor.ContributorFragment import io.github.wulkanowy.ui.modules.about.license.LicenseFragment @@ -19,17 +16,16 @@ import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.getCompatDrawable import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_about.* import javax.inject.Inject -class AboutFragment : BaseFragment(), AboutView, MainView.TitledView { +class AboutFragment : BaseFragment(R.layout.fragment_about), AboutView, + MainView.TitledView { @Inject lateinit var presenter: AboutPresenter @Inject - lateinit var aboutAdapter: FlexibleAdapter> + lateinit var aboutAdapter: AboutAdapter @Inject lateinit var appInfo: AppInfo @@ -80,29 +76,25 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView { fun newInstance() = AboutFragment() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_about, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding = FragmentAboutBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - aboutAdapter.setOnItemClickListener(presenter::onItemSelected) + aboutAdapter.onClickListener = presenter::onItemSelected - with(aboutRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.aboutRecycler) { + layoutManager = LinearLayoutManager(context) adapter = aboutAdapter } } - override fun updateData(header: AboutScrollableHeader, items: List) { + override fun updateData(data: List>) { with(aboutAdapter) { - removeAllScrollableHeaders() - addScrollableHeader(header) - updateDataSet(items) + items = data + notifyDataSetChanged() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutItem.kt deleted file mode 100644 index 29f1cd8c..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.item_about - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>) = ViewHolder(view, adapter) - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList) { - 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>) : FlexibleViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View? get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt index 7e740b32..27237ea6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.about -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -23,10 +22,9 @@ class AboutPresenter @Inject constructor( loadData() } - fun onItemSelected(item: AbstractFlexibleItem<*>) { - if (item !is AboutItem) return + fun onItemSelected(name: String) { view?.run { - when (item.title) { + when (name) { versionRes?.first -> { Timber.i("Opening log viewer") openLogViewer() @@ -73,15 +71,16 @@ class AboutPresenter @Inject constructor( private fun loadData() { view?.run { - updateData(AboutScrollableHeader(), listOfNotNull( - versionRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, - creatorsRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, - feedbackRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, - faqRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, - discordRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, - homepageRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, - licensesRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, - privacyRes?.let { (title, summary, image) -> AboutItem(title, summary, image) })) + updateData(listOfNotNull( + versionRes, + creatorsRes, + feedbackRes, + faqRes, + discordRes, + homepageRes, + licensesRes, + privacyRes + )) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutScrollableHeader.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutScrollableHeader.kt deleted file mode 100644 index 07bb4124..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutScrollableHeader.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.scrollable_header_about - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>) = ViewHolder(view, adapter) - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList) { - 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>) : FlexibleViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View? get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutView.kt index 4bc0c3fe..79b700ea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutView.kt @@ -23,7 +23,7 @@ interface AboutView : BaseView { fun initView() - fun updateData(header: AboutScrollableHeader, items: List) + fun updateData(data: List>) fun openLogViewer() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorAdapter.kt new file mode 100644 index 00000000..215cd27d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorAdapter.kt @@ -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() { + + var items = emptyList() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorFragment.kt index c181c3d3..42eebd34 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorFragment.kt @@ -1,30 +1,27 @@ package io.github.wulkanowy.ui.modules.about.contributor import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.FlexibleItemDecoration -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.data.pojos.Contributor +import io.github.wulkanowy.databinding.FragmentContributorBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.openInternetBrowser -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_creator.* import javax.inject.Inject -class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView { +class ContributorFragment : BaseFragment(R.layout.fragment_contributor), + ContributorView, MainView.TitledView { @Inject lateinit var presenter: ContributorPresenter @Inject - lateinit var creatorsAdapter: FlexibleAdapter> + lateinit var creatorsAdapter: ContributorAdapter override val titleStringId get() = R.string.contributors_title @@ -32,29 +29,27 @@ class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView fun newInstance() = ContributorFragment() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_creator, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentContributorBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - with(creatorRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.creatorRecycler) { + layoutManager = LinearLayoutManager(context) adapter = creatorsAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider() - .withDrawDividerOnLastItem(false)) + addItemDecoration(DividerItemDecoration(context)) } - creatorsAdapter.setOnItemClickListener(presenter::onItemSelected) - creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() } + creatorsAdapter.onClickListener = presenter::onItemSelected + binding.creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() } } - override fun updateData(data: List) { - creatorsAdapter.updateDataSet(data) + override fun updateData(data: List) { + with(creatorsAdapter) { + items = data + notifyDataSetChanged() + } } override fun openUserGithubPage(username: String) { @@ -66,7 +61,7 @@ class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView } override fun showProgress(show: Boolean) { - creatorProgress.visibility = if (show) VISIBLE else GONE + binding.creatorProgress.visibility = if (show) VISIBLE else GONE } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorItem.kt deleted file mode 100644 index 844b5bd8..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.item_contributor - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>) = ViewHolder(view, adapter) - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList) { - 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>) : FlexibleViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View? get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorPresenter.kt index 721b2500..416a59ce 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorPresenter.kt @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.about.contributor -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.data.repositories.appcreator.AppCreatorRepository import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter @@ -21,9 +21,8 @@ class ContributorPresenter @Inject constructor( loadData() } - fun onItemSelected(item: AbstractFlexibleItem<*>) { - if (item !is ContributorItem) return - view?.openUserGithubPage(item.creator.githubUsername) + fun onItemSelected(contributor: Contributor) { + view?.openUserGithubPage(contributor.githubUsername) } fun onSeeMoreClick() { @@ -32,7 +31,6 @@ class ContributorPresenter @Inject constructor( private fun loadData() { disposable.add(appCreatorRepository.getAppCreators() - .map { it.map { creator -> ContributorItem(creator) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { view?.showProgress(false) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorView.kt index 18ec3a8e..8007e4e3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorView.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.about.contributor +import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.ui.base.BaseView interface ContributorView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) fun openUserGithubPage(username: String) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseAdapter.kt new file mode 100644 index 00000000..07025c09 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseAdapter.kt @@ -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() { + + var items = emptyList() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseFragment.kt index 2681680b..f6c3b569 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseFragment.kt @@ -1,33 +1,29 @@ package io.github.wulkanowy.ui.modules.about.license import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.core.text.parseAsHtml +import androidx.recyclerview.widget.LinearLayoutManager import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.entity.Library import dagger.Lazy -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentLicenseBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainView -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_license.* import javax.inject.Inject -class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView { +class LicenseFragment : BaseFragment(R.layout.fragment_license), + LicenseView, MainView.TitledView { @Inject lateinit var presenter: LicensePresenter @Inject - lateinit var licenseAdapter: FlexibleAdapter> + lateinit var licenseAdapter: LicenseAdapter @Inject lateinit var libs: Lazy @@ -43,25 +39,26 @@ class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView { fun newInstance() = LicenseFragment() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_license, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLicenseBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - with(licenseRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + licenseAdapter.onClickListener = presenter::onItemSelected + + with(binding.licenseRecycler) { + layoutManager = LinearLayoutManager(context) adapter = licenseAdapter } - licenseAdapter.setOnItemClickListener(presenter::onItemSelected) } - override fun updateData(data: List) { - licenseAdapter.updateDataSet(data) + override fun updateData(data: List) { + with(licenseAdapter) { + items = data + notifyDataSetChanged() + } } override fun openLicense(licenseHtml: String) { @@ -76,7 +73,7 @@ class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView { } override fun showProgress(show: Boolean) { - licenseProgress.visibility = if (show) VISIBLE else GONE + binding.licenseProgress.visibility = if (show) VISIBLE else GONE } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseItem.kt deleted file mode 100644 index 8dcb8922..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.item_license - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>) = ViewHolder(view, adapter) - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList) { - 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>) : FlexibleViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View? get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt index dc48b098..d0f6d69e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.about.license -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import com.mikepenz.aboutlibraries.entity.Library import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -20,14 +20,12 @@ class LicensePresenter @Inject constructor( loadData() } - fun onItemSelected(item: AbstractFlexibleItem<*>) { - if (item !is LicenseItem) return - view?.run { item.library.license?.licenseDescription?.let { openLicense(it) } } + fun onItemSelected(library: Library) { + view?.run { library.license?.licenseDescription?.let { openLicense(it) } } } private fun loadData() { - disposable.add(Single.fromCallable { view?.appLibraries } - .map { it.map { library -> LicenseItem(library) } } + disposable.add(Single.fromCallable { view?.appLibraries.orEmpty() } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doOnEvent { _, _ -> view?.showProgress(false) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseView.kt index 3939d3e8..0680dbb7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseView.kt @@ -9,7 +9,7 @@ interface LicenseView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) fun openLicense(licenseHtml: String) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/logviewer/LogViewerFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/logviewer/LogViewerFragment.kt index 0b7b05c7..08f91aff 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/logviewer/LogViewerFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/logviewer/LogViewerFragment.kt @@ -8,23 +8,22 @@ import android.net.Uri import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.view.ViewGroup import androidx.core.content.FileProvider import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.BuildConfig.APPLICATION_ID import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentLogviewerBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainView -import kotlinx.android.synthetic.main.fragment_logviewer.* import java.io.File import javax.inject.Inject -class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView { +class LogViewerFragment : BaseFragment(R.layout.fragment_logviewer), + LogViewerView, MainView.TitledView { @Inject lateinit var presenter: LogViewerPresenter @@ -43,13 +42,10 @@ class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView { setHasOptionsMenu(true) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_logviewer, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = logViewerRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLogviewerBinding.bind(view) + messageContainer = binding.logViewerRecycler presenter.onAttachView(this) } @@ -63,18 +59,18 @@ class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView { } override fun initView() { - with(logViewerRecycler) { + with(binding.logViewerRecycler) { layoutManager = LinearLayoutManager(context) adapter = logAdapter } - logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() } + binding.logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() } } override fun setLines(lines: List) { logAdapter.lines = lines logAdapter.notifyDataSetChanged() - logViewerRecycler.scrollToPosition(lines.size - 1) + binding.logViewerRecycler.scrollToPosition(lines.size - 1) } override fun shareLogs(files: List) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountAdapter.kt new file mode 100644 index 00000000..7df0ca37 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountAdapter.kt @@ -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() { + + var items = emptyList() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountDialog.kt index cfff31c9..ce811e0b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountDialog.kt @@ -7,23 +7,21 @@ import android.view.ViewGroup import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.databinding.DialogAccountBinding import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.modules.login.LoginActivity -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.dialog_account.* import javax.inject.Inject -class AccountDialog : BaseDialogFragment(), AccountView { +class AccountDialog : BaseDialogFragment(), AccountView { @Inject lateinit var presenter: AccountPresenter @Inject - lateinit var accountAdapter: FlexibleAdapter> + lateinit var accountAdapter: AccountAdapter companion object { fun newInstance() = AccountDialog() @@ -35,7 +33,7 @@ class AccountDialog : BaseDialogFragment(), AccountView { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_account, container, false) + return DialogAccountBinding.inflate(inflater).apply { binding = this }.root } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -44,18 +42,23 @@ class AccountDialog : BaseDialogFragment(), AccountView { } override fun initView() { - accountAdapter.setOnItemClickListener { presenter.onItemSelected(it) } + accountAdapter.onClickListener = presenter::onItemSelected - accountDialogAdd.setOnClickListener { presenter.onAddSelected() } - accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() } - accountDialogRecycler.apply { - layoutManager = SmoothScrollLinearLayoutManager(context) - adapter = accountAdapter + with(binding) { + accountDialogAdd.setOnClickListener { presenter.onAddSelected() } + accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() } + accountDialogRecycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = accountAdapter + } } } - override fun updateData(data: List) { - accountAdapter.updateDataSet(data) + override fun updateData(data: List) { + with(accountAdapter) { + items = data + notifyDataSetChanged() + } } override fun showError(text: String, error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountItem.kt deleted file mode 100644 index d3a3ee6a..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.item_account - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>) = ViewHolder(view, adapter) - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - 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 - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt index e9b4b81e..3416a043 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.account -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter @@ -63,32 +63,29 @@ class AccountPresenter @Inject constructor( })) } - fun onItemSelected(item: AbstractFlexibleItem<*>) { - if (item is AccountItem) { - Timber.i("Select student item ${item.student.id}") - if (item.student.isCurrent) { - view?.dismissView() - } else { - Timber.i("Attempt to change a student") - disposable.add(studentRepository.switchStudent(item.student) - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .doFinally { view?.dismissView() } - .subscribe({ - Timber.i("Change a student result: Success") - view?.recreateMainView() - }, { - Timber.i("Change a student result: An exception occurred") - errorHandler.dispatch(it) - })) - } + fun onItemSelected(student: Student) { + Timber.i("Select student item ${student.id}") + if (student.isCurrent) { + view?.dismissView() + } else { + Timber.i("Attempt to change a student") + disposable.add(studentRepository.switchStudent(student) + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doFinally { view?.dismissView() } + .subscribe({ + Timber.i("Change a student result: Success") + view?.recreateMainView() + }, { + Timber.i("Change a student result: An exception occurred") + errorHandler.dispatch(it) + })) } } private fun loadData() { Timber.i("Loading account data started") disposable.add(studentRepository.getSavedStudents(false) - .map { it.map { item -> AccountItem(item) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .subscribe({ @@ -100,4 +97,3 @@ class AccountPresenter @Inject constructor( })) } } - diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt index ede5023b..abb9e1d2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.account +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.ui.base.BaseView interface AccountView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) fun dismissView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index 75f99840..a63d5045 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -1,12 +1,80 @@ package io.github.wulkanowy.ui.modules.attendance -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.repositories.attendance.SentExcuseStatus +import io.github.wulkanowy.databinding.ItemAttendanceBinding +import javax.inject.Inject -class AttendanceAdapter> : FlexibleAdapter(null, null, true) { +class AttendanceAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = emptyList() var excuseActionMode: Boolean = false + var onClickListener: (Attendance) -> Unit = {} + var onExcuseCheckboxSelect: (attendanceItem: Attendance, checked: Boolean) -> Unit = { _, _ -> } + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemAttendanceBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val item = items[position] + + with(holder.binding) { + attendanceItemNumber.text = item.number.toString() + attendanceItemSubject.text = item.subject + attendanceItemDescription.text = item.name + attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE } + attendanceItemNumber.visibility = View.GONE + attendanceItemExcuseInfo.visibility = View.GONE + attendanceItemExcuseCheckbox.visibility = View.GONE + attendanceItemExcuseCheckbox.isChecked = false + attendanceItemExcuseCheckbox.setOnCheckedChangeListener { _, checked -> + onExcuseCheckboxSelect(item, checked) + } + + when (if (item.excuseStatus != null) SentExcuseStatus.valueOf(item.excuseStatus) else null) { + SentExcuseStatus.WAITING -> { + attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting) + attendanceItemExcuseInfo.visibility = View.VISIBLE + attendanceItemAlert.visibility = View.INVISIBLE + } + SentExcuseStatus.DENIED -> { + attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied) + attendanceItemExcuseInfo.visibility = View.VISIBLE + } + else -> { + if (item.excusable && excuseActionMode) { + attendanceItemNumber.visibility = View.GONE + attendanceItemExcuseCheckbox.visibility = View.VISIBLE + } else { + attendanceItemNumber.visibility = View.VISIBLE + attendanceItemExcuseCheckbox.visibility = View.GONE + } + } + } + root.setOnClickListener { + onClickListener(item) + + with(attendanceItemExcuseCheckbox) { + if (excuseActionMode && isVisible) { + isChecked = !isChecked + } + } + } + } + } + + class ItemViewHolder(val binding: ItemAttendanceBinding) : RecyclerView.ViewHolder(binding.root) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt index 611dd999..97b76e81 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt @@ -5,13 +5,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment -import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.databinding.DialogAttendanceBinding +import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.synthetic.main.dialog_attendance.* class AttendanceDialog : DialogFragment() { + private var binding: DialogAttendanceBinding by lifecycleAwareVariable() + private lateinit var attendance: Attendance companion object { @@ -33,16 +35,18 @@ class AttendanceDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_attendance, container, false) + return DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - attendanceDialogSubject.text = attendance.subject - attendanceDialogDescription.text = attendance.name - attendanceDialogDate.text = attendance.date.toFormattedString() - attendanceDialogNumber.text = attendance.number.toString() - attendanceDialogClose.setOnClickListener { dismiss() } + with(binding) { + attendanceDialogSubject.text = attendance.subject + attendanceDialogDescription.text = attendance.name + attendanceDialogDate.text = attendance.date.toFormattedString() + attendanceDialogNumber.text = attendance.number.toString() + attendanceDialogClose.setOnClickListener { dismiss() } + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 9969b1c7..6599243d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -10,35 +10,32 @@ import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.ActionMode +import androidx.recyclerview.widget.LinearLayoutManager import com.wdullaer.materialdatetimepicker.date.DatePickerDialog -import eu.davidea.flexibleadapter.common.FlexibleItemDecoration -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.databinding.DialogExcuseBinding +import io.github.wulkanowy.databinding.FragmentAttendanceBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.SchooldaysRangeLimiter import io.github.wulkanowy.utils.dpToPx -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.dialog_excuse.* -import kotlinx.android.synthetic.main.fragment_attendance.* import org.threeten.bp.LocalDate import javax.inject.Inject -class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildView, +class AttendanceFragment : BaseFragment(R.layout.fragment_attendance), AttendanceView, MainView.MainChildView, MainView.TitledView { @Inject lateinit var presenter: AttendancePresenter @Inject - lateinit var attendanceAdapter: AttendanceAdapter> + lateinit var attendanceAdapter: AttendanceAdapter override val excuseSuccessString: String get() = getString(R.string.attendance_excuse_success) @@ -54,7 +51,7 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie override val titleStringId get() = R.string.attendance_title - override val isViewEmpty get() = attendanceAdapter.isEmpty + override val isViewEmpty get() = attendanceAdapter.items.isEmpty() override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize @@ -91,39 +88,38 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie setHasOptionsMenu(true) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_attendance, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = attendanceRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentAttendanceBinding.bind(view) + messageContainer = binding.attendanceRecycler presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - attendanceAdapter.setOnItemClickListener(presenter::onAttendanceItemSelected) - attendanceAdapter.onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect - - with(attendanceRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) - adapter = attendanceAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider() - .withDrawDividerOnLastItem(false)) + with(attendanceAdapter) { + onClickListener = presenter::onAttendanceItemSelected + onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect } - attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - attendanceErrorRetry.setOnClickListener { presenter.onRetry() } - attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding.attendanceRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = attendanceAdapter + addItemDecoration(DividerItemDecoration(context)) + } - attendancePreviousButton.setOnClickListener { presenter.onPreviousDay() } - attendanceNavDate.setOnClickListener { presenter.onPickDate() } - attendanceNextButton.setOnClickListener { presenter.onNextDay() } + with(binding) { + attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + attendanceErrorRetry.setOnClickListener { presenter.onRetry() } + attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } - attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } + attendancePreviousButton.setOnClickListener { presenter.onPreviousDay() } + attendanceNavDate.setOnClickListener { presenter.onPickDate() } + attendanceNextButton.setOnClickListener { presenter.onNextDay() } - attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } + + attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -135,20 +131,26 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie else false } - override fun updateData(data: List) { - attendanceAdapter.updateDataSet(data, true) + override fun updateData(data: List) { + with(attendanceAdapter) { + items = data + notifyDataSetChanged() + } } override fun updateNavigationDay(date: String) { - attendanceNavDate.text = date + binding.attendanceNavDate.text = date } override fun clearData() { - attendanceAdapter.clear() + with(attendanceAdapter) { + items = emptyList() + notifyDataSetChanged() + } } override fun resetView() { - attendanceRecycler.smoothScrollToPosition(0) + binding.attendanceRecycler.smoothScrollToPosition(0) } override fun onFragmentReselected() { @@ -164,43 +166,43 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie } override fun showEmpty(show: Boolean) { - attendanceEmpty.visibility = if (show) VISIBLE else GONE + binding.attendanceEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - attendanceError.visibility = if (show) VISIBLE else GONE + binding.attendanceError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - attendanceErrorMessage.text = message + binding.attendanceErrorMessage.text = message } override fun showProgress(show: Boolean) { - attendanceProgress.visibility = if (show) VISIBLE else GONE + binding.attendanceProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - attendanceSwipe.isEnabled = enable + binding.attendanceSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - attendanceRecycler.visibility = if (show) VISIBLE else GONE + binding. attendanceRecycler.visibility = if (show) VISIBLE else GONE } override fun hideRefresh() { - attendanceSwipe.isRefreshing = false + binding.attendanceSwipe.isRefreshing = false } override fun showPreButton(show: Boolean) { - attendancePreviousButton.visibility = if (show) VISIBLE else INVISIBLE + binding.attendancePreviousButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showNextButton(show: Boolean) { - attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE + binding. attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showExcuseButton(show: Boolean) { - attendanceExcuseButton.visibility = if (show) VISIBLE else GONE + binding.attendanceExcuseButton.visibility = if (show) VISIBLE else GONE } override fun showAttendanceDialog(lesson: Attendance) { @@ -223,14 +225,15 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie } override fun showExcuseDialog() { + val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context)) AlertDialog.Builder(requireContext()) .setTitle(R.string.attendance_excuse_title) - .setView(R.layout.dialog_excuse) + .setView(dialogBinding.root) .setNegativeButton(android.R.string.cancel) { _, _ -> } .create() .apply { setButton(BUTTON_POSITIVE, getString(R.string.attendance_excuse_dialog_submit)) { _, _ -> - presenter.onExcuseDialogSubmit(excuseReason.text?.toString().orEmpty()) + presenter.onExcuseDialogSubmit(dialogBinding.excuseReason.text?.toString().orEmpty()) } }.show() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceItem.kt deleted file mode 100644 index 7355aec2..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.item_attendance - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - 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 - } - } - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceModule.kt deleted file mode 100644 index eb35fea1..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceModule.kt +++ /dev/null @@ -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>() -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index 7fc04474..3a1fb0ce 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.attendance import android.annotation.SuppressLint -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.repositories.attendance.AttendanceRepository import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository @@ -111,11 +110,11 @@ class AttendancePresenter @Inject constructor( view?.finishActionMode() } - fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) { + fun onAttendanceItemSelected(attendance: Attendance) { view?.apply { - if (item is AttendanceItem && !excuseActionMode) { - Timber.i("Select attendance item ${item.attendance.id}") - showAttendanceDialog(item.attendance) + if (!excuseActionMode) { + Timber.i("Select attendance item ${attendance.id}") + showAttendanceDialog(attendance) } } } @@ -197,9 +196,7 @@ class AttendancePresenter @Inject constructor( if (prefRepository.isShowPresent) list else list.filter { !it.presence } } - .delay(200, MILLISECONDS) - .map { items -> items.map { AttendanceItem(it) } } - .map { items -> items.sortedBy { it.attendance.number } } + .map { items -> items.sortedBy { it.number } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -216,7 +213,7 @@ class AttendancePresenter @Inject constructor( showEmpty(it.isEmpty()) showErrorView(false) showContent(it.isNotEmpty()) - showExcuseButton(it.any { item -> item.attendance.excusable }) + showExcuseButton(it.any { item -> item.excusable }) } analytics.logEvent("load_attendance", "items" to it.size, "force_refresh" to forceRefresh) }) { @@ -236,7 +233,6 @@ class AttendancePresenter @Inject constructor( attendanceRepository.excuseForAbsence(student, semester, toExcuseList, reason) } } - .delay(200, MILLISECONDS) .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doOnSubscribe { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt index 03e95053..484070a2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt @@ -18,7 +18,7 @@ interface AttendanceView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) fun updateNavigationDay(date: String) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryAdapter.kt new file mode 100644 index 00000000..236c3da1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryAdapter.kt @@ -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() { + + private enum class ViewType(val id: Int) { + HEADER(1), + ITEM(2) + } + + var items = emptyList() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt index 8e30ea8b..1b9fe08e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt @@ -1,32 +1,31 @@ package io.github.wulkanowy.ui.modules.attendance.summary import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.TextView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.AttendanceSummary +import io.github.wulkanowy.databinding.FragmentAttendanceSummaryBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.setOnItemSelectedListener -import kotlinx.android.synthetic.main.fragment_attendance_summary.* import javax.inject.Inject -class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainView.TitledView { +class AttendanceSummaryFragment : + BaseFragment(R.layout.fragment_attendance_summary), + AttendanceSummaryView, MainView.TitledView { @Inject lateinit var presenter: AttendanceSummaryPresenter @Inject - lateinit var attendanceSummaryAdapter: FlexibleAdapter> + lateinit var attendanceSummaryAdapter: AttendanceSummaryAdapter private lateinit var subjectsAdapter: ArrayAdapter @@ -36,41 +35,38 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie fun newInstance() = AttendanceSummaryFragment() } - override val totalString get() = getString(R.string.attendance_summary_total) - override val titleStringId get() = R.string.attendance_title - override val isViewEmpty get() = attendanceSummaryAdapter.isEmpty + override val isViewEmpty get() = attendanceSummaryAdapter.items.isEmpty() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_attendance_summary, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = attendanceSummaryRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentAttendanceSummaryBinding.bind(view) + messageContainer = binding.attendanceSummaryRecycler presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SUBJECT_KEY)) } override fun initView() { - with(attendanceSummaryRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.attendanceSummaryRecycler) { + layoutManager = LinearLayoutManager(context) adapter = attendanceSummaryAdapter } - attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) - attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() } - attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) + attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() } + attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } + } subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject) - with(attendanceSummarySubjects) { + with(binding.attendanceSummarySubjects) { adapter = subjectsAdapter setOnItemSelectedListener { presenter.onSubjectSelected(it?.text?.toString()) } } - attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) } override fun updateSubjects(data: ArrayList) { @@ -81,48 +77,50 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie } } - override fun updateDataSet(data: List, header: AttendanceSummaryScrollableHeader) { + override fun updateDataSet(data: List) { with(attendanceSummaryAdapter) { - updateDataSet(data, true) - removeAllScrollableHeaders() - addScrollableHeader(header) + items = data + notifyDataSetChanged() } } override fun clearView() { - attendanceSummaryAdapter.clear() + with(attendanceSummaryAdapter) { + items = emptyList() + notifyDataSetChanged() + } } override fun showEmpty(show: Boolean) { - attendanceSummaryEmpty.visibility = if (show) VISIBLE else GONE + binding.attendanceSummaryEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - attendanceSummaryError.visibility = if (show) VISIBLE else GONE + binding.attendanceSummaryError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - attendanceSummaryErrorMessage.text = message + binding.attendanceSummaryErrorMessage.text = message } override fun showProgress(show: Boolean) { - attendanceSummaryProgress.visibility = if (show) VISIBLE else GONE + binding.attendanceSummaryProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - attendanceSummarySwipe.isEnabled = enable + binding.attendanceSummarySwipe.isEnabled = enable } override fun showContent(show: Boolean) { - attendanceSummaryRecycler.visibility = if (show) VISIBLE else GONE + binding.attendanceSummaryRecycler.visibility = if (show) VISIBLE else GONE } override fun showSubjects(show: Boolean) { - attendanceSummarySubjectsContainer.visibility = if (show) VISIBLE else INVISIBLE + binding.attendanceSummarySubjectsContainer.visibility = if (show) VISIBLE else INVISIBLE } override fun hideRefresh() { - attendanceSummarySwipe.isRefreshing = false + binding.attendanceSummarySwipe.isRefreshing = false } override fun onSaveInstanceState(outState: Bundle) { @@ -131,7 +129,7 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryItem.kt deleted file mode 100644 index 265d6ce4..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.item_attendance_summary - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - 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 - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt index 8fc5b6e4..72dfc327 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.attendance.summary -import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.Subject import io.github.wulkanowy.data.repositories.attendancesummary.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository @@ -10,13 +9,8 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.SchedulersProvider -import io.github.wulkanowy.utils.calculatePercentage -import io.github.wulkanowy.utils.getFormattedName import org.threeten.bp.Month import timber.log.Timber -import java.lang.String.format -import java.util.Locale.FRANCE -import java.util.concurrent.TimeUnit.MILLISECONDS import javax.inject.Inject class AttendanceSummaryPresenter @Inject constructor( @@ -88,8 +82,7 @@ class AttendanceSummaryPresenter @Inject constructor( attendanceSummaryRepository.getAttendanceSummary(student, it, subjectId, forceRefresh) } } - .map { createAttendanceSummaryItems(it) to AttendanceSummaryScrollableHeader(formatPercentage(it.calculatePercentage())) } - .delay(200, MILLISECONDS) + .map { items -> items.sortedByDescending { if (it.month.value <= Month.JUNE.value) it.month.value + 12 else it.month.value } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -102,11 +95,11 @@ class AttendanceSummaryPresenter @Inject constructor( .subscribe({ Timber.i("Loading attendance summary result: Success") view?.apply { - showEmpty(it.first.isEmpty()) - showContent(it.first.isNotEmpty()) - updateDataSet(it.first, it.second) + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + updateDataSet(it) } - analytics.logEvent("load_attendance_summary", "items" to it.first.size, "force_refresh" to forceRefresh, "item_id" to subjectId) + analytics.logEvent("load_attendance_summary", "items" to it.size, "force_refresh" to forceRefresh, "item_id" to subjectId) }) { Timber.i("Loading attendance summary result: An exception occurred") errorHandler.dispatch(it) @@ -150,42 +143,4 @@ class AttendanceSummaryPresenter @Inject constructor( }) ) } - - private fun createAttendanceSummaryTotalItem(attendanceSummary: List): 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): List { - 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)}%" - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryScrollableHeader.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryScrollableHeader.kt deleted file mode 100644 index c258f71d..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryScrollableHeader.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.scrollable_header_attendance_summary - - override fun createViewHolder(view: View?, adapter: FlexibleAdapter>?): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: ViewHolder?, position: Int, payloads: MutableList?) { - 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>?) : FlexibleViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View? - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryView.kt index 8bd5332d..dd4053c7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryView.kt @@ -1,11 +1,10 @@ package io.github.wulkanowy.ui.modules.attendance.summary +import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.ui.base.BaseView interface AttendanceSummaryView : BaseView { - val totalString: String - val isViewEmpty: Boolean fun initView() @@ -24,7 +23,7 @@ interface AttendanceSummaryView : BaseView { fun setErrorDetails(message: String) - fun updateDataSet(data: List, header: AttendanceSummaryScrollableHeader) + fun updateDataSet(data: List) fun updateSubjects(data: ArrayList) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamAdapter.kt new file mode 100644 index 00000000..85061997 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamAdapter.kt @@ -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() { + + var items = emptyList>() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamDialog.kt index ed5092c9..c1a6f1f0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamDialog.kt @@ -5,13 +5,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment -import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam +import io.github.wulkanowy.databinding.DialogExamBinding +import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.synthetic.main.dialog_exam.* class ExamDialog : DialogFragment() { + private var binding: DialogExamBinding by lifecycleAwareVariable() + private lateinit var exam: Exam companion object { @@ -33,18 +35,20 @@ class ExamDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_exam, container, false) + return DialogExamBinding.inflate(inflater).apply { binding = this }.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - examDialogSubjectValue.text = exam.subject - examDialogTypeValue.text = exam.type - examDialogTeacherValue.text = exam.teacher - examDialogDateValue.text = exam.entryDate.toFormattedString() - examDialogDescriptionValue.text = exam.description + with(binding) { + examDialogSubjectValue.text = exam.subject + examDialogTypeValue.text = exam.type + examDialogTeacherValue.text = exam.teacher + examDialogDateValue.text = exam.entryDate.toFormattedString() + examDialogDescriptionValue.text = exam.description - examDialogClose.setOnClickListener { dismiss() } + examDialogClose.setOnClickListener { dismiss() } + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index b880f465..eeab30c1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -1,33 +1,29 @@ package io.github.wulkanowy.ui.modules.exam import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.FlexibleItemDecoration -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam +import io.github.wulkanowy.databinding.FragmentExamBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.dpToPx -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_exam.* import javax.inject.Inject -class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.TitledView { +class ExamFragment : BaseFragment(R.layout.fragment_exam), ExamView, + MainView.MainChildView, MainView.TitledView { @Inject lateinit var presenter: ExamPresenter @Inject - lateinit var examAdapter: FlexibleAdapter> + lateinit var examAdapter: ExamAdapter companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" @@ -37,57 +33,60 @@ class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView. override val titleStringId get() = R.string.exam_title - override val isViewEmpty get() = examAdapter.isEmpty + override val isViewEmpty get() = examAdapter.items.isEmpty() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_exam, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = examRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentExamBinding.bind(view) + messageContainer = binding.examRecycler presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - examAdapter.setOnItemClickListener(presenter::onExamItemSelected) + examAdapter.onClickListener = presenter::onExamItemSelected - with(examRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.examRecycler) { + layoutManager = LinearLayoutManager(context) adapter = examAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider(R.layout.item_exam) - .withDrawDividerOnLastItem(false)) + addItemDecoration(DividerItemDecoration(context)) } - examSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - examErrorRetry.setOnClickListener { presenter.onRetry() } - examErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + examSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + examErrorRetry.setOnClickListener { presenter.onRetry() } + examErrorDetails.setOnClickListener { presenter.onDetailsClick() } - examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } - examNextButton.setOnClickListener { presenter.onNextWeek() } + examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } + examNextButton.setOnClickListener { presenter.onNextWeek() } - examNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + examNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + } } override fun hideRefresh() { - examSwipe.isRefreshing = false + binding.examSwipe.isRefreshing = false } - override fun updateData(data: List) { - examAdapter.updateDataSet(data, true) + override fun updateData(data: List>) { + with(examAdapter) { + items = data + notifyDataSetChanged() + } } override fun updateNavigationWeek(date: String) { - examNavDate.text = date + binding.examNavDate.text = date } override fun clearData() { - examAdapter.clear() + with(examAdapter) { + items = emptyList() + notifyDataSetChanged() + } } override fun resetView() { - examRecycler.scrollToPosition(0) + binding.examRecycler.scrollToPosition(0) } override fun onFragmentReselected() { @@ -95,35 +94,35 @@ class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView. } override fun showEmpty(show: Boolean) { - examEmpty.visibility = if (show) VISIBLE else GONE + binding.examEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - examError.visibility = if (show) VISIBLE else GONE + binding.examError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - examErrorMessage.text = message + binding.examErrorMessage.text = message } override fun showProgress(show: Boolean) { - examProgress.visibility = if (show) VISIBLE else GONE + binding.examProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - examSwipe.isEnabled = enable + binding.examSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - examRecycler.visibility = if (show) VISIBLE else GONE + binding.examRecycler.visibility = if (show) VISIBLE else GONE } override fun showPreButton(show: Boolean) { - examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE + binding.examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showNextButton(show: Boolean) { - examNextButton.visibility = if (show) VISIBLE else INVISIBLE + binding.examNextButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showExamDialog(exam: Exam) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamHeader.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamHeader.kt deleted file mode 100644 index 0a5b862c..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamHeader.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.header_exam - - override fun createViewHolder(view: View?, adapter: FlexibleAdapter>?): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: ViewHolder, - position: Int, payloads: MutableList?) { - 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>?) : ExpandableViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamItem.kt index 8971b4df..579e3720 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamItem.kt @@ -1,50 +1,9 @@ package io.github.wulkanowy.ui.modules.exam -import android.view.View -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractSectionableItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.davidea.viewholders.FlexibleViewHolder -import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.Exam -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_exam.* +data class ExamItem(val value: T, val viewType: ViewType) { -class ExamItem(header: ExamHeader, val exam: Exam) : AbstractSectionableItem(header) { - - override fun getLayoutRes() = R.layout.item_exam - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - holder.run { - examItemSubject.text = exam.subject - examItemTeacher.text = exam.teacher - examItemType.text = exam.type - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ExamItem - - if (exam != other.exam) return false - - return true - } - - override fun hashCode(): Int { - var result = exam.hashCode() - result = 31 * result + exam.id.toInt() - return result - } - - class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer { - override val containerView: View - get() = contentView + enum class ViewType(val id: Int) { + HEADER(1), + ITEM(2) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt index aac9bc4b..495602fc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.exam -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.repositories.exam.ExamRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository @@ -19,7 +18,6 @@ import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.ofEpochDay import timber.log.Timber -import java.util.concurrent.TimeUnit.MILLISECONDS import javax.inject.Inject class ExamPresenter @Inject constructor( @@ -75,11 +73,9 @@ class ExamPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - fun onExamItemSelected(item: AbstractFlexibleItem<*>?) { - if (item is ExamItem) { - Timber.i("Select exam item ${item.exam.id}") - view?.showExamDialog(item.exam) - } + fun onExamItemSelected(exam: Exam) { + Timber.i("Select exam item ${exam.id}") + view?.showExamDialog(exam) } fun onViewReselected() { @@ -117,8 +113,6 @@ class ExamPresenter @Inject constructor( examRepository.getExams(student, semester, currentDate.monday, currentDate.friday, forceRefresh) } } - .delay(200, MILLISECONDS) - .map { it.groupBy { exam -> exam.date }.toSortedMap() } .map { createExamItems(it) } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) @@ -156,12 +150,12 @@ class ExamPresenter @Inject constructor( } } - private fun createExamItems(items: Map>): List { - return items.flatMap { - ExamHeader(it.key).let { header -> - it.value.reversed().map { item -> ExamItem(header, item) } + private fun createExamItems(items: List): List> { + return items.groupBy { it.date }.toSortedMap().map { (date, exams) -> + listOf(ExamItem(date, ExamItem.ViewType.HEADER)) + exams.reversed().map { exam -> + ExamItem(exam, ExamItem.ViewType.ITEM) } - } + }.flatten() } private fun reloadView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt index 5f4a7430..00429bae 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt @@ -9,7 +9,7 @@ interface ExamView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List>) fun updateNavigationWeek(date: String) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index 3bb084d3..954b5756 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -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.Student import io.github.wulkanowy.data.repositories.grade.GradeRepository -import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository +import io.github.wulkanowy.data.repositories.semester.SemesterRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.changeModifier -import io.reactivex.Maybe import io.reactivex.Single import javax.inject.Inject class GradeAverageProvider @Inject constructor( - private val preferencesRepository: PreferencesRepository, + private val semesterRepository: SemesterRepository, private val gradeRepository: GradeRepository, - private val gradeSummaryRepository: GradeSummaryRepository + private val preferencesRepository: PreferencesRepository ) { private val plusModifier = preferencesRepository.gradePlusModifier private val minusModifier = preferencesRepository.gradeMinusModifier - fun getGradeAverage(student: Student, semesters: List, selectedSemesterId: Int, forceRefresh: Boolean): Single>> { - return when (preferencesRepository.gradeAverageMode) { - "all_year" -> getAllYearAverage(student, semesters, selectedSemesterId, forceRefresh) - "only_one_semester" -> getOnlyOneSemesterAverage(student, semesters, selectedSemesterId, forceRefresh) - else -> throw IllegalArgumentException("Incorrect grade average mode: ${preferencesRepository.gradeAverageMode} ") + fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean = false): Single> { + return semesterRepository.getSemesters(student).flatMap { semesters -> + when (preferencesRepository.gradeAverageMode) { + "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} ") + } } } - private fun getAllYearAverage(student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean): Single>> { + private fun calculateWholeYearAverage(student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean): Single> { val selectedSemester = semesters.single { it.semesterId == semesterId } val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } - return getAverageFromGradeSummary(student, selectedSemester, forceRefresh) - .switchIfEmpty(gradeRepository.getGrades(student, selectedSemester, forceRefresh) - .flatMap { firstGrades -> - if (selectedSemester == firstSemester) Single.just(firstGrades) - else { - gradeRepository.getGrades(student, firstSemester) - .map { secondGrades -> secondGrades + firstGrades } + return getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh).flatMap { selectedDetails -> + val isAnyAverage = selectedDetails.any { it.average != .0 } + + if (selectedSemester != firstSemester) { + getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails -> + selectedDetails.map { selected -> + val second = secondDetails.singleOrNull { it.subject == selected.subject } + selected.copy( + average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) { + (selected.grades + second?.grades.orEmpty()).calcAverage() + } else (selected.average + (second?.average ?: selected.average)) / 2 + ) } - }.map { grades -> - grades.map { if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier) else it } - .groupBy { it.subject } - .map { Triple(it.key, it.value.calcAverage(), "") } - }) + } + } else Single.just(selectedDetails) + } } - private fun getOnlyOneSemesterAverage(student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean): Single>> { - val selectedSemester = semesters.single { it.semesterId == semesterId } + private fun getSemesterDetailsWithAverage(student: Student, semester: Semester, forceRefresh: Boolean): Single> { + 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) - .switchIfEmpty(gradeRepository.getGrades(student, selectedSemester, forceRefresh) - .map { grades -> - grades.map { if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier) else it } - .groupBy { it.subject } - .map { Triple(it.key, it.value.calcAverage(), "") } - }) - } - - private fun getAverageFromGradeSummary(student: Student, selectedSemester: Semester, forceRefresh: Boolean): Maybe>> { - 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 } + summaries.map { summary -> + val grades = allGrades[summary.subject].orEmpty() + GradeDetailsWithAverage( + subject = summary.subject, + average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) { + 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 + ) + } + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeDetailsWithAverage.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeDetailsWithAverage.kt new file mode 100644 index 00000000..3f5d706b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeDetailsWithAverage.kt @@ -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 +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index b1faf18f..59b83c4b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -1,16 +1,15 @@ package io.github.wulkanowy.ui.modules.grade import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentGradeBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter import io.github.wulkanowy.ui.modules.grade.details.GradeDetailsFragment @@ -19,10 +18,9 @@ import io.github.wulkanowy.ui.modules.grade.summary.GradeSummaryFragment import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.setOnSelectPageListener -import kotlinx.android.synthetic.main.fragment_grade.* import javax.inject.Inject -class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainView.TitledView { +class GradeFragment : BaseFragment(R.layout.fragment_grade), GradeView, MainView.MainChildView, MainView.TitledView { @Inject lateinit var presenter: GradePresenter @@ -42,19 +40,16 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie override var subtitleString = "" - override val currentPageIndex get() = gradeViewPager.currentItem + override val currentPageIndex get() = binding.gradeViewPager.currentItem override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_grade, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentGradeBinding.bind(view) presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY)) } @@ -66,7 +61,7 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie override fun initView() { with(pagerAdapter) { - containerId = gradeViewPager.id + containerId = binding.gradeViewPager.id addFragmentsWithTitle(mapOf( GradeDetailsFragment.newInstance() to getString(R.string.all_details), GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), @@ -74,19 +69,21 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie )) } - with(gradeViewPager) { + with(binding.gradeViewPager) { adapter = pagerAdapter offscreenPageLimit = 3 setOnSelectPageListener(presenter::onPageSelected) } - with(gradeTabLayout) { - setupWithViewPager(gradeViewPager) + with(binding.gradeTabLayout) { + setupWithViewPager(binding.gradeViewPager) setElevationCompat(context.dpToPx(4f)) } - gradeErrorRetry.setOnClickListener { presenter.onRetry() } - gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + gradeErrorRetry.setOnClickListener { presenter.onRetry() } + gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() } + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -99,20 +96,22 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie } override fun showContent(show: Boolean) { - gradeViewPager.visibility = if (show) VISIBLE else INVISIBLE - gradeTabLayout.visibility = if (show) VISIBLE else INVISIBLE + with(binding) { + gradeViewPager.visibility = if (show) VISIBLE else INVISIBLE + gradeTabLayout.visibility = if (show) VISIBLE else INVISIBLE + } } override fun showProgress(show: Boolean) { - gradeProgress.visibility = if (show) VISIBLE else INVISIBLE + binding.gradeProgress.visibility = if (show) VISIBLE else INVISIBLE } override fun showErrorView(show: Boolean) { - gradeError.visibility = if (show) VISIBLE else INVISIBLE + binding.gradeError.visibility = if (show) VISIBLE else INVISIBLE } override fun setErrorDetails(message: String) { - gradeErrorMessage.text = message + binding.gradeErrorMessage.text = message } override fun showSemesterSwitch(show: Boolean) { @@ -166,7 +165,7 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt new file mode 100644 index 00000000..7adab547 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt @@ -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() { + + private var headers = mutableListOf() + + private var items = mutableListOf() + + private var expandedPosition = RecyclerView.NO_POSITION + + private var isExpandable = false + + var onClickListener: (Grade, position: Int) -> Unit = { _, _ -> } + + var colorTheme = "" + + fun setDataItems(data: List, 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) { + 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, private val new: List) : + 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] + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt index a8f8a865..698aff3e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt @@ -8,14 +8,17 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.databinding.DialogGradeBinding import io.github.wulkanowy.utils.colorStringId import io.github.wulkanowy.utils.getBackgroundColor import io.github.wulkanowy.utils.getGradeColor +import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.synthetic.main.dialog_grade.* class GradeDetailsDialog : DialogFragment() { + private var binding: DialogGradeBinding by lifecycleAwareVariable() + private lateinit var grade: Grade private lateinit var colorScheme: String @@ -44,47 +47,49 @@ class GradeDetailsDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_grade, container, false) + return DialogGradeBinding.inflate(inflater).apply { binding = this }.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - gradeDialogSubject.text = grade.subject + with(binding) { + gradeDialogSubject.text = grade.subject - gradeDialogColorAndWeightValue.run { - text = context.getString(R.string.grade_weight_value, grade.weight) - setBackgroundResource(grade.getGradeColor()) - } - - gradeDialogDateValue.text = grade.date.toFormattedString() - gradeDialogColorValue.text = getString(grade.colorStringId) - - gradeDialogCommentValue.apply { - if (grade.comment.isBlank()) { - visibility = GONE - gradeDialogComment.visibility = GONE - } else text = grade.comment - } - - gradeDialogValue.run { - text = grade.entry - setBackgroundResource(grade.getBackgroundColor(colorScheme)) - } - - gradeDialogTeacherValue.text = if (grade.teacher.isBlank()) { - getString(R.string.all_no_data) - } else grade.teacher - - gradeDialogDescriptionValue.text = grade.run { - when { - description.isBlank() && gradeSymbol.isNotBlank() -> gradeSymbol - description.isBlank() && gradeSymbol.isBlank() -> getString(R.string.all_no_description) - gradeSymbol.isNotBlank() && description.isNotBlank() -> "$gradeSymbol - $description" - else -> description + gradeDialogColorAndWeightValue.run { + text = context.getString(R.string.grade_weight_value, grade.weight) + setBackgroundResource(grade.getGradeColor()) } - } - gradeDialogClose.setOnClickListener { dismiss() } + gradeDialogDateValue.text = grade.date.toFormattedString() + gradeDialogColorValue.text = getString(grade.colorStringId) + + gradeDialogCommentValue.apply { + if (grade.comment.isBlank()) { + visibility = GONE + gradeDialogComment.visibility = GONE + } else text = grade.comment + } + + gradeDialogValue.run { + text = grade.entry + setBackgroundResource(grade.getBackgroundColor(colorScheme)) + } + + gradeDialogTeacherValue.text = if (grade.teacher.isBlank()) { + getString(R.string.all_no_data) + } else grade.teacher + + gradeDialogDescriptionValue.text = grade.run { + when { + description.isBlank() && gradeSymbol.isNotBlank() -> gradeSymbol + description.isBlank() && gradeSymbol.isBlank() -> getString(R.string.all_no_description) + gradeSymbol.isNotBlank() && description.isNotBlank() -> "$gradeSymbol - $description" + else -> description + } + } + + gradeDialogClose.setOnClickListener { dismiss() } + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt index 9505d354..ac50b9f5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.grade.details import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem @@ -9,29 +8,25 @@ import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IExpandable -import eu.davidea.flexibleadapter.items.IFlexible +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.databinding.FragmentGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_grade_details.* import javax.inject.Inject -class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeChildView { +class GradeDetailsFragment : + BaseFragment(R.layout.fragment_grade_details), GradeDetailsView, + GradeView.GradeChildView { @Inject lateinit var presenter: GradeDetailsPresenter @Inject - lateinit var gradeDetailsAdapter: FlexibleAdapter> + lateinit var gradeDetailsAdapter: GradeDetailsAdapter private var gradeDetailsMenu: Menu? = null @@ -39,36 +34,18 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh fun newInstance() = GradeDetailsFragment() } - override val emptyAverageString: String - get() = getString(R.string.grade_no_average) - - override val averageString: String - get() = getString(R.string.grade_average) - - override val pointsSumString: String - get() = getString(R.string.grade_points_sum) - - override val weightString: String - get() = getString(R.string.grade_weight) - - override val noDescriptionString: String - get() = getString(R.string.all_no_description) - override val isViewEmpty - get() = gradeDetailsAdapter.isEmpty + get() = gradeDetailsAdapter.itemCount == 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_grade_details, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = gradeDetailsRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentGradeDetailsBinding.bind(view) + messageContainer = binding.gradeDetailsRecycler presenter.onAttachView(this) } @@ -79,22 +56,17 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh } override fun initView() { - gradeDetailsAdapter.run { - isAutoCollapseOnExpand = true - isAutoScrollOnExpand = true - setOnItemClickListener { presenter.onGradeItemSelected(it) } - } + gradeDetailsAdapter.onClickListener = presenter::onGradeItemSelected - gradeDetailsRecycler.run { - layoutManager = SmoothScrollLinearLayoutManager(context) - adapter = gradeDetailsAdapter - addItemDecoration(GradeDetailsHeaderItemDecoration(context) - .withDefaultDivider(R.layout.header_grade_details) - ) + with(binding) { + with(gradeDetailsRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = gradeDetailsAdapter + } + gradeDetailsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + gradeDetailsErrorRetry.setOnClickListener { presenter.onRetry() } + gradeDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - gradeDetailsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - gradeDetailsErrorRetry.setOnClickListener { presenter.onRetry() } - gradeDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -102,16 +74,23 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh else false } - override fun updateData(data: List) { - gradeDetailsAdapter.updateDataSet(data, true) + override fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) { + with(gradeDetailsAdapter) { + colorTheme = gradeColorTheme + setDataItems(data, isGradeExpandable) + notifyDataSetChanged() + } } - override fun updateItem(item: AbstractFlexibleItem<*>) { - gradeDetailsAdapter.updateItem(item) + override fun updateItem(item: Grade, position: Int) { + gradeDetailsAdapter.updateDetailsItem(position, item) } override fun clearView() { - gradeDetailsAdapter.clear() + with(gradeDetailsAdapter) { + setDataItems(mutableListOf()) + notifyDataSetChanged() + } } override fun collapseAllItems() { @@ -119,43 +98,43 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh } override fun scrollToStart() { - gradeDetailsRecycler.scrollToPosition(0) + binding.gradeDetailsRecycler.smoothScrollToPosition(0) } - override fun getHeaderOfItem(item: AbstractFlexibleItem<*>): IExpandable<*, out IFlexible<*>>? { - return gradeDetailsAdapter.getExpandableOf(item) + override fun getHeaderOfItem(subject: String): GradeDetailsItem { + return gradeDetailsAdapter.getHeaderItem(subject) } - override fun getGradeNumberString(number: Int): String { - return resources.getQuantityString(R.plurals.grade_number_item, number, number) + override fun updateHeaderItem(item: GradeDetailsItem) { + gradeDetailsAdapter.updateHeaderItem(item) } override fun showProgress(show: Boolean) { - gradeDetailsProgress.visibility = if (show) VISIBLE else GONE + binding.gradeDetailsProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - gradeDetailsSwipe.isEnabled = enable + binding.gradeDetailsSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - gradeDetailsRecycler.visibility = if (show) VISIBLE else INVISIBLE + binding.gradeDetailsRecycler.visibility = if (show) VISIBLE else INVISIBLE } override fun showEmpty(show: Boolean) { - gradeDetailsEmpty.visibility = if (show) VISIBLE else INVISIBLE + binding.gradeDetailsEmpty.visibility = if (show) VISIBLE else INVISIBLE } override fun showErrorView(show: Boolean) { - gradeDetailsError.visibility = if (show) VISIBLE else GONE + binding.gradeDetailsError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - gradeDetailsErrorMessage.text = message + binding.gradeDetailsErrorMessage.text = message } override fun showRefresh(show: Boolean) { - gradeDetailsSwipe.isRefreshing = show + binding.gradeDetailsSwipe.isRefreshing = show } override fun showGradeDialog(grade: Grade, colorScheme: String) { @@ -187,7 +166,7 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsHeader.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsHeader.kt deleted file mode 100644 index 4a34a145..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsHeader.kt +++ /dev/null @@ -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() { - - init { - isExpanded = !isExpandable - } - - override fun getLayoutRes() = R.layout.header_grade_details - - override fun createViewHolder(view: View?, adapter: FlexibleAdapter>?): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: ViewHolder, position: Int, payloads: MutableList?) { - 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>?) : - 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) } - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsHeaderItemDecoration.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsHeaderItemDecoration.kt deleted file mode 100644 index 39a911e6..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsHeaderItemDecoration.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsItem.kt index 1e47eca5..28197496 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsItem.kt @@ -1,74 +1,19 @@ package io.github.wulkanowy.ui.modules.grade.details -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.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() { - - override fun getLayoutRes() = R.layout.item_grade_details - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - 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 - } +enum class ViewType(val id: Int) { + HEADER(1), + ITEM(2) } + +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 +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt index a9e5b2b7..26c22264 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -1,6 +1,5 @@ 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.repositories.grade.GradeRepository 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.ErrorHandler 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.SchedulersProvider -import io.github.wulkanowy.utils.getBackgroundColor import timber.log.Timber import javax.inject.Inject @@ -43,24 +42,20 @@ class GradeDetailsPresenter @Inject constructor( loadData(semesterId, forceRefresh) } - fun onGradeItemSelected(item: AbstractFlexibleItem<*>?) { - if (item is GradeDetailsItem) { - Timber.i("Select grade item ${item.grade.id}") - view?.apply { - showGradeDialog(item.grade, preferencesRepository.gradeColorTheme) - if (!item.grade.isRead) { - item.grade.isRead = true - updateItem(item) - getHeaderOfItem(item)?.let { header -> - if (header is GradeDetailsHeader) { - header.newGrades-- - updateItem(header) - } - } - newGradesAmount-- - updateMarkAsDoneButton() - updateGrade(item.grade) + fun onGradeItemSelected(grade: Grade, position: Int) { + Timber.i("Select grade item ${grade.id}") + view?.apply { + showGradeDialog(grade, preferencesRepository.gradeColorTheme) + if (!grade.isRead) { + grade.isRead = true + updateItem(grade, position) + getHeaderOfItem(grade.subject).let { header -> + (header.value as GradeDetailsHeader).newGrades-- + updateHeaderItem(header) } + newGradesAmount-- + updateMarkAsDoneButton() + updateGrade(grade) } } } @@ -132,16 +127,7 @@ class GradeDetailsPresenter @Inject constructor( private fun loadData(semesterId: Int, forceRefresh: Boolean) { Timber.i("Loading grade details data started") disposable.add(studentRepository.getCurrentStudent() - .flatMap { semesterRepository.getSemesters(it).map { semester -> it to semester } } - .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) } - } - } + .flatMap { averageProvider.getGradesDetailsWithAverage(it, semesterId, forceRefresh) } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -152,17 +138,21 @@ class GradeDetailsPresenter @Inject constructor( notifyParentDataLoaded(semesterId) } } - .subscribe({ + .subscribe({ grades -> 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() view?.run { - showEmpty(it.isEmpty()) + showEmpty(grades.isEmpty()) showErrorView(false) - showContent(it.isNotEmpty()) - updateData(it) + showContent(grades.isNotEmpty()) + 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") errorHandler.dispatch(it) @@ -180,40 +170,20 @@ class GradeDetailsPresenter @Inject constructor( } } - private fun createGradeItems(items: Map>, averages: List>): List { - val isGradeExpandable = preferencesRepository.isGradeExpandable - val gradeColorTheme = preferencesRepository.gradeColorTheme - - 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 createGradeItems(items: List): List { + return items.filter { it.grades.isNotEmpty() }.map { (subject, average, points, _, grades) -> + val subItems = grades.map { + GradeDetailsItem(it, ViewType.ITEM) } - } - } - private fun formatAverage(average: Double?): String { - return view?.run { - if (average == null || average == .0) emptyAverageString - else averageString.format(average) - }.orEmpty() + listOf(GradeDetailsItem(GradeDetailsHeader( + subject = subject, + average = average, + pointsSum = points, + newGrades = grades.filter { grade -> !grade.isRead }.size, + grades = subItems + ), ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems + }.flatten() } private fun updateGrade(grade: Grade) { @@ -221,8 +191,9 @@ class GradeDetailsPresenter @Inject constructor( disposable.add(gradeRepository.updateGrade(grade) .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) - .subscribe({ Timber.i("Update grade result: Success") }) - { error -> + .subscribe({ + Timber.i("Update grade result: Success") + }) { error -> Timber.i("Update grade result: An exception occurred") errorHandler.dispatch(error) }) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt index e2977bcb..e71fcc3c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt @@ -1,8 +1,5 @@ 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.ui.base.BaseView @@ -10,21 +7,13 @@ interface GradeDetailsView : BaseView { val isViewEmpty: Boolean - val emptyAverageString: String - - val averageString: String - - val pointsSumString: String - - val weightString: String - - val noDescriptionString: String - fun initView() - fun updateData(data: List) + fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) - fun updateItem(item: AbstractFlexibleItem<*>) + fun updateItem(item: Grade, position: Int) + + fun updateHeaderItem(item: GradeDetailsItem) fun clearView() @@ -54,7 +43,5 @@ interface GradeDetailsView : BaseView { fun enableMarkAsDoneButton(enable: Boolean) - fun getGradeNumberString(number: Int): String - - fun getHeaderOfItem(item: AbstractFlexibleItem<*>): IExpandable<*, out IFlexible<*>>? + fun getHeaderOfItem(subject: String): GradeDetailsItem } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt index a7b7b653..dbb60910 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.modules.grade.statistics import android.graphics.Color import android.view.LayoutInflater -import android.view.View import android.view.View.GONE import android.view.View.VISIBLE 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.GradeStatistics 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 kotlinx.android.synthetic.main.item_grade_statistics_bar.view.* -import kotlinx.android.synthetic.main.item_grade_statistics_pie.view.* import javax.inject.Inject class GradeStatisticsAdapter @Inject constructor() : @@ -33,6 +32,8 @@ class GradeStatisticsAdapter @Inject constructor() : var theme: String = "vulcan" + var showAllSubjectsOnList: Boolean = false + private val vulcanGradeColors = listOf( 6 to R.color.grade_vulcan_six, 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+" ) - 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 { - 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 getItemViewType(position: Int) = items[position].type.id 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) { - R.layout.item_grade_statistics_bar -> GradeStatisticsBar(viewHolder) - else -> GradeStatisticsPie(viewHolder) + ViewType.PARTIAL.id, ViewType.SEMESTER.id -> PieViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)) + ViewType.POINTS.id -> BarViewHolder(ItemGradeStatisticsBarBinding.inflate(inflater, parent, false)) + else -> throw IllegalStateException() } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is GradeStatisticsPie -> bindPieChart(holder, items[position].partial) - is GradeStatisticsBar -> bindBarChart(holder, items[position].points!!) + is PieViewHolder -> bindPieChart(holder, items[position].partial) + is BarViewHolder -> bindBarChart(holder, items[position].points!!) } } - private fun bindPieChart(holder: GradeStatisticsPie, partials: List) { - with(holder.view.gradeStatisticsPieTitle) { + private fun bindPieChart(holder: PieViewHolder, partials: List) { + with(holder.binding.gradeStatisticsPieTitle) { 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) { @@ -105,10 +102,10 @@ class GradeStatisticsAdapter @Inject constructor() : valueTextColor = Color.WHITE setColors(partials.map { 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) if (partials.size == 1) animateXY(1000, 1000) data = PieData(dataset).apply { @@ -140,8 +137,8 @@ class GradeStatisticsAdapter @Inject constructor() : } } - private fun bindBarChart(holder: GradeStatisticsBar, points: GradePointsStatistics) { - with(holder.view.gradeStatisticsBarTitle) { + private fun bindBarChart(holder: BarViewHolder, points: GradePointsStatistics) { + with(holder.binding.gradeStatisticsBarTitle) { text = points.subject visibility = if (items.size == 1) GONE else VISIBLE } @@ -153,14 +150,14 @@ class GradeStatisticsAdapter @Inject constructor() : with(dataset) { valueTextSize = 12f - valueTextColor = holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary) + valueTextColor = holder.binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary) valueFormatter = object : ValueFormatter() { override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%" } colors = gradePointsColors } - with(holder.view.gradeStatisticsBar) { + with(holder.binding.gradeStatisticsBar) { setTouchEnabled(false) if (items.size == 1) animateXY(1000, 1000) data = BarData(dataset).apply { @@ -183,7 +180,7 @@ class GradeStatisticsAdapter @Inject constructor() : 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 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) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt index c05cf380..3a8f4007 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt @@ -1,23 +1,23 @@ package io.github.wulkanowy.ui.modules.grade.statistics import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.TextView import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R 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.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.setOnItemSelectedListener -import kotlinx.android.synthetic.main.fragment_grade_statistics.* import javax.inject.Inject -class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.GradeChildView { +class GradeStatisticsFragment : + BaseFragment(R.layout.fragment_grade_statistics), + GradeStatisticsView, GradeView.GradeChildView { @Inject lateinit var presenter: GradeStatisticsPresenter @@ -36,24 +36,21 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G override val isViewEmpty get() = statisticsAdapter.items.isEmpty() override val currentType - get() = when (gradeStatisticsTypeSwitch.checkedRadioButtonId) { + get() = when (binding.gradeStatisticsTypeSwitch.checkedRadioButtonId) { R.id.gradeStatisticsTypeSemester -> ViewType.SEMESTER R.id.gradeStatisticsTypePartial -> ViewType.PARTIAL else -> ViewType.POINTS } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_grade_statistics, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = gradeStatisticsSwipe + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentGradeStatisticsBinding.bind(view) + messageContainer = binding.gradeStatisticsSwipe presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? ViewType) } override fun initView() { - with(gradeStatisticsRecycler) { + with(binding.gradeStatisticsRecycler) { layoutManager = LinearLayoutManager(requireContext()) adapter = statisticsAdapter } @@ -61,16 +58,18 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject) - with(gradeStatisticsSubjects) { + with(binding.gradeStatisticsSubjects) { adapter = subjectsAdapter setOnItemSelectedListener { presenter.onSubjectSelected(it?.text?.toString()) } } - gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + with(binding) { + gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) - gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() } - gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() } + gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() } + gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() } + } } override fun updateSubjects(data: ArrayList) { @@ -81,15 +80,17 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G } } - override fun updateData(items: List, theme: String) { - statisticsAdapter.theme = theme - statisticsAdapter.items = items - statisticsAdapter.notifyDataSetChanged() + override fun updateData(items: List, theme: String, showAllSubjectsOnStatisticsList: Boolean) { + with(statisticsAdapter) { + this.showAllSubjectsOnList = showAllSubjectsOnStatisticsList + this.theme = theme + this.items = items + notifyDataSetChanged() + } } override fun showSubjects(show: Boolean) { - gradeStatisticsSubjectsContainer.visibility = if (show) View.VISIBLE else View.INVISIBLE - gradeStatisticsTypeSwitch.visibility = if (show) View.VISIBLE else View.INVISIBLE + binding.gradeStatisticsSubjectsContainer.visibility = if (show) View.VISIBLE else View.GONE } override fun clearView() { @@ -97,35 +98,35 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G } override fun resetView() { - gradeStatisticsScroll.scrollTo(0, 0) + binding.gradeStatisticsScroll.scrollTo(0, 0) } 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) { - gradeStatisticsEmpty.visibility = if (show) View.VISIBLE else View.INVISIBLE + binding.gradeStatisticsEmpty.visibility = if (show) View.VISIBLE else View.INVISIBLE } 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) { - gradeStatisticsErrorMessage.text = message + binding.gradeStatisticsErrorMessage.text = message } 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) { - gradeStatisticsSwipe.isEnabled = enable + binding.gradeStatisticsSwipe.isEnabled = enable } override fun showRefresh(show: Boolean) { - gradeStatisticsSwipe.isRefreshing = show + binding.gradeStatisticsSwipe.isRefreshing = show } override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { @@ -150,7 +151,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G override fun onResume() { super.onResume() - gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() } + binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() } } override fun onSaveInstanceState(outState: Bundle) { @@ -159,7 +160,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt index 90c4e38e..5cc733cd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt @@ -128,10 +128,7 @@ class GradeStatisticsPresenter @Inject constructor( .observeOn(schedulers.mainThread) .subscribe({ Timber.i("Loading grade stats subjects result: Success") - view?.run { - updateSubjects(it) - showSubjects(true) - } + view?.updateSubjects(it) }, { Timber.i("Loading grade stats subjects result: An exception occurred") errorHandler.dispatch(it) @@ -140,22 +137,21 @@ class GradeStatisticsPresenter @Inject constructor( } private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) { - currentSubjectName = subjectName + currentSubjectName = if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName 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") disposable.add(studentRepository.getCurrentStudent() .flatMap { student -> semesterRepository.getSemesters(student).flatMap { semesters -> val semester = semesters.first { item -> item.semesterId == semesterId } - when (type) { - ViewType.SEMESTER -> gradeStatisticsRepository.getGradesStatistics(student, semester, subjectName, true, forceRefresh) - ViewType.PARTIAL -> gradeStatisticsRepository.getGradesStatistics(student, semester, subjectName, false, forceRefresh) - ViewType.POINTS -> gradeStatisticsRepository.getGradesPointsStatistics(student, semester, subjectName, forceRefresh) + with(gradeStatisticsRepository) { + when (type) { + ViewType.SEMESTER -> getGradesStatistics(student, semester, currentSubjectName, true, forceRefresh) + ViewType.PARTIAL -> getGradesStatistics(student, semester, currentSubjectName, false, forceRefresh) + ViewType.POINTS -> getGradesPointsStatistics(student, semester, currentSubjectName, forceRefresh) + } } } } @@ -175,7 +171,8 @@ class GradeStatisticsPresenter @Inject constructor( showEmpty(it.isEmpty()) showContent(it.isNotEmpty()) 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) }) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt index 9ba8524c..26b4a119 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt @@ -13,7 +13,7 @@ interface GradeStatisticsView : BaseView { fun updateSubjects(data: ArrayList) - fun updateData(items: List, theme: String) + fun updateData(items: List, theme: String, showAllSubjectsOnStatisticsList: Boolean) fun showSubjects(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/ViewType.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/ViewType.kt index 08c37d58..02e95b0e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/ViewType.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/ViewType.kt @@ -1,7 +1,7 @@ package io.github.wulkanowy.ui.modules.grade.statistics -enum class ViewType { - SEMESTER, - PARTIAL, - POINTS +enum class ViewType(val id: Int) { + SEMESTER(1), + PARTIAL(2), + POINTS(3) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt new file mode 100644 index 00000000..30c4ccc2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt @@ -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() { + + private enum class ViewType(val id: Int) { + HEADER(1), + ITEM(2) + } + + var items = emptyList() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt index 05fde522..d169f7c6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt @@ -1,36 +1,35 @@ package io.github.wulkanowy.ui.modules.grade.summary import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.GradeSummary +import io.github.wulkanowy.databinding.FragmentGradeSummaryBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeView -import kotlinx.android.synthetic.main.fragment_grade_summary.* import javax.inject.Inject -class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeChildView { +class GradeSummaryFragment : + BaseFragment(R.layout.fragment_grade_summary), GradeSummaryView, + GradeView.GradeChildView { @Inject lateinit var presenter: GradeSummaryPresenter @Inject - lateinit var gradeSummaryAdapter: FlexibleAdapter> + lateinit var gradeSummaryAdapter: GradeSummaryAdapter companion object { fun newInstance() = GradeSummaryFragment() } override val isViewEmpty - get() = gradeSummaryAdapter.isEmpty + get() = gradeSummaryAdapter.items.isEmpty() override val predictedString get() = getString(R.string.grade_summary_predicted_grade) @@ -38,70 +37,69 @@ class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeCh override val finalString get() = getString(R.string.grade_summary_final_grade) - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_grade_summary, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = gradeSummaryRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentGradeSummaryBinding.bind(view) + messageContainer = binding.gradeSummaryRecycler presenter.onAttachView(this) } override fun initView() { - gradeSummaryAdapter.setDisplayHeadersAtStartUp(true) - - gradeSummaryRecycler.run { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.gradeSummaryRecycler) { + layoutManager = LinearLayoutManager(context) adapter = gradeSummaryAdapter } - gradeSummarySwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() } - gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + gradeSummarySwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() } + gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } + } } - override fun updateData(data: List, header: GradeSummaryScrollableHeader) { - gradeSummaryAdapter.apply { - updateDataSet(data, true) - removeAllScrollableHeaders() - addScrollableHeader(header) + override fun updateData(data: List) { + with(gradeSummaryAdapter) { + items = data + notifyDataSetChanged() } } override fun clearView() { - gradeSummaryAdapter.clear() + with(gradeSummaryAdapter) { + items = emptyList() + notifyDataSetChanged() + } } override fun resetView() { - gradeSummaryRecycler.scrollToPosition(0) + binding.gradeSummaryRecycler.scrollToPosition(0) } 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) { - gradeSummaryEmpty.visibility = if (show) VISIBLE else INVISIBLE + binding.gradeSummaryEmpty.visibility = if (show) VISIBLE else INVISIBLE } 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) { - gradeSummaryErrorMessage.text = message + binding.gradeSummaryErrorMessage.text = message } 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) { - gradeSummarySwipe.isEnabled = enable + binding.gradeSummarySwipe.isEnabled = enable } override fun showRefresh(show: Boolean) { - gradeSummarySwipe.isRefreshing = show + binding.gradeSummarySwipe.isRefreshing = show } override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { @@ -125,7 +123,7 @@ class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeCh } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt deleted file mode 100644 index 95c32d17..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.item_grade_summary - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - 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 - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt index c12f2a51..9b837213 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt @@ -1,26 +1,20 @@ package io.github.wulkanowy.ui.modules.grade.summary 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.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler 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.SchedulersProvider -import io.github.wulkanowy.utils.calcAverage import timber.log.Timber -import java.lang.String.format -import java.util.Locale.FRANCE import javax.inject.Inject class GradeSummaryPresenter @Inject constructor( schedulers: SchedulersProvider, errorHandler: ErrorHandler, studentRepository: StudentRepository, - private val gradeSummaryRepository: GradeSummaryRepository, - private val semesterRepository: SemesterRepository, private val averageProvider: GradeAverageProvider, private val analytics: FirebaseAnalyticsHelper ) : BasePresenter(errorHandler, studentRepository, schedulers) { @@ -36,15 +30,8 @@ class GradeSummaryPresenter @Inject constructor( fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) { Timber.i("Loading grade summary data started") disposable.add(studentRepository.getCurrentStudent() - .flatMap { semesterRepository.getSemesters(it).map { semesters -> it to semesters } } - .flatMap { (student, semesters) -> - 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) } - } - } + .flatMap { averageProvider.getGradesDetailsWithAverage(it, semesterId, forceRefresh) } + .map { createGradeSummaryItems(it) } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -54,15 +41,15 @@ class GradeSummaryPresenter @Inject constructor( enableSwipe(true) notifyParentDataLoaded(semesterId) } - }.subscribe({ (gradeSummaryItems, gradeSummaryHeader) -> + }.subscribe({ Timber.i("Loading grade summary result: Success") view?.run { - showEmpty(gradeSummaryItems.isEmpty()) - showContent(gradeSummaryItems.isNotEmpty()) + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) 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") errorHandler.dispatch(it) @@ -115,21 +102,10 @@ class GradeSummaryPresenter @Inject constructor( disposable.clear() } - private fun createGradeSummaryItemsAndHeader(gradesSummary: List, averages: List>): Pair, GradeSummaryScrollableHeader> { - return averages.filter { value -> value.second != 0.0 } - .let { filteredAverages -> - 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())) - } - } + private fun createGradeSummaryItems(items: List): List { + return items.map { + it.summary.copy(average = it.average) + } } private fun checkEmpty(gradeSummary: GradeSummary, averages: List>): Boolean { @@ -137,9 +113,4 @@ class GradeSummaryPresenter @Inject constructor( 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) - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryScrollableHeader.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryScrollableHeader.kt deleted file mode 100644 index f1c535c7..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryScrollableHeader.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes() = R.layout.scrollable_header_grade_summary - - override fun createViewHolder(view: View?, adapter: FlexibleAdapter>?): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: ViewHolder?, - position: Int, payloads: MutableList?) { - 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>?) : FlexibleViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View? - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt index cf318487..974d9141 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.grade.summary +import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.ui.base.BaseView interface GradeSummaryView : BaseView { @@ -12,7 +13,7 @@ interface GradeSummaryView : BaseView { fun initView() - fun updateData(data: List, header: GradeSummaryScrollableHeader) + fun updateData(data: List) fun resetView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkAdapter.kt new file mode 100644 index 00000000..a87ad18e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkAdapter.kt @@ -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() { + + var items = emptyList>() + + 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) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index a011a015..f3052957 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -1,33 +1,29 @@ package io.github.wulkanowy.ui.modules.homework import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.FlexibleItemDecoration -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.databinding.FragmentHomeworkBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.homework.details.HomeworkDetailsDialog import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.dpToPx -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_homework.* import javax.inject.Inject -class HomeworkFragment : BaseFragment(), HomeworkView, MainView.TitledView { +class HomeworkFragment : BaseFragment(R.layout.fragment_homework), + HomeworkView, MainView.TitledView { @Inject lateinit var presenter: HomeworkPresenter @Inject - lateinit var homeworkAdapter: FlexibleAdapter> + lateinit var homeworkAdapter: HomeworkAdapter companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" @@ -37,41 +33,41 @@ class HomeworkFragment : BaseFragment(), HomeworkView, MainView.TitledView { override val titleStringId get() = R.string.homework_title - override val isViewEmpty get() = homeworkAdapter.isEmpty + override val isViewEmpty get() = homeworkAdapter.items.isEmpty() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_homework, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = homeworkRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentHomeworkBinding.bind(view) + messageContainer = binding.homeworkRecycler presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - homeworkAdapter.setOnItemClickListener(presenter::onHomeworkItemSelected) + homeworkAdapter.onClickListener = presenter::onHomeworkItemSelected - with(homeworkRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.homeworkRecycler) { + layoutManager = LinearLayoutManager(context) adapter = homeworkAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider() - .withDrawDividerOnLastItem(false)) + addItemDecoration(DividerItemDecoration(context)) } - homeworkSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - homeworkErrorRetry.setOnClickListener { presenter.onRetry() } - homeworkErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + homeworkSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + homeworkErrorRetry.setOnClickListener { presenter.onRetry() } + homeworkErrorDetails.setOnClickListener { presenter.onDetailsClick() } - homeworkPreviousButton.setOnClickListener { presenter.onPreviousDay() } - homeworkNextButton.setOnClickListener { presenter.onNextDay() } + homeworkPreviousButton.setOnClickListener { presenter.onPreviousDay() } + homeworkNextButton.setOnClickListener { presenter.onNextDay() } - homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + } } - override fun updateData(data: List) { - homeworkAdapter.updateDataSet(data, true) + override fun updateData(data: List>) { + with(homeworkAdapter) { + items = data + notifyDataSetChanged() + } } fun onReloadList() { @@ -79,47 +75,50 @@ class HomeworkFragment : BaseFragment(), HomeworkView, MainView.TitledView { } override fun clearData() { - homeworkAdapter.clear() + with(homeworkAdapter) { + items = emptyList() + notifyDataSetChanged() + } } override fun updateNavigationWeek(date: String) { - homeworkNavDate.text = date + binding.homeworkNavDate.text = date } override fun hideRefresh() { - homeworkSwipe.isRefreshing = false + binding.homeworkSwipe.isRefreshing = false } override fun showEmpty(show: Boolean) { - homeworkEmpty.visibility = if (show) VISIBLE else GONE + binding.homeworkEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - homeworkError.visibility = if (show) VISIBLE else GONE + binding.homeworkError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - homeworkErrorMessage.text = message + binding.homeworkErrorMessage.text = message } override fun showProgress(show: Boolean) { - homeworkProgress.visibility = if (show) VISIBLE else GONE + binding.homeworkProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - homeworkSwipe.isEnabled = enable + binding.homeworkSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - homeworkRecycler.visibility = if (show) VISIBLE else GONE + binding.homeworkRecycler.visibility = if (show) VISIBLE else GONE } override fun showPreButton(show: Boolean) { - homeworkPreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE + binding.homeworkPreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE } override fun showNextButton(show: Boolean) { - homeworkNextButton.visibility = if (show) VISIBLE else View.INVISIBLE + binding.homeworkNextButton.visibility = if (show) VISIBLE else View.INVISIBLE } override fun showTimetableDialog(homework: Homework) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkHeader.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkHeader.kt deleted file mode 100644 index 49023788..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkHeader.kt +++ /dev/null @@ -1,54 +0,0 @@ -package io.github.wulkanowy.ui.modules.homework - -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_homework.* -import org.threeten.bp.LocalDate - -class HomeworkHeader(private val date: LocalDate) : AbstractHeaderItem() { - - override fun getLayoutRes() = R.layout.header_homework - - override fun createViewHolder(view: View?, adapter: FlexibleAdapter>?): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>?, holder: HomeworkHeader.ViewHolder, - position: Int, payloads: MutableList? - ) { - holder.run { - homeworkHeaderDay.text = date.weekDayName.capitalize() - homeworkHeaderDate.text = date.toFormattedString() - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as HomeworkHeader - - if (date != other.date) return false - - return true - } - - override fun hashCode(): Int { - return date.hashCode() - } - - class ViewHolder(view: View?, adapter: FlexibleAdapter>?) : ExpandableViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkItem.kt index 3c2dd7ba..7e003958 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkItem.kt @@ -1,53 +1,9 @@ package io.github.wulkanowy.ui.modules.homework -import android.view.View -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractSectionableItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.davidea.viewholders.FlexibleViewHolder -import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.Homework -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_homework.* +data class HomeworkItem(val value: T, val viewType: ViewType) { -class HomeworkItem(header: HomeworkHeader, val homework: Homework) : - AbstractSectionableItem(header) { - - override fun getLayoutRes() = R.layout.item_homework - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - holder.apply { - 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 - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as HomeworkItem - - if (homework != other.homework) return false - return true - } - - override fun hashCode(): Int { - var result = homework.hashCode() - result = 31 * result + homework.id.toInt() - return result - } - - class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : - FlexibleViewHolder(view, adapter), LayoutContainer { - override val containerView: View - get() = contentView + enum class ViewType(val id: Int) { + HEADER(1), + ITEM(2) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt index aae18df3..d39efde1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.homework -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.repositories.homework.HomeworkRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository @@ -18,7 +17,6 @@ import io.github.wulkanowy.utils.toFormattedString import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate.ofEpochDay import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject class HomeworkPresenter @Inject constructor( @@ -74,11 +72,9 @@ class HomeworkPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - fun onHomeworkItemSelected(item: AbstractFlexibleItem<*>?) { - if (item is HomeworkItem) { - Timber.i("Select homework item ${item.homework.id}") - view?.showTimetableDialog(item.homework) - } + fun onHomeworkItemSelected(homework: Homework) { + Timber.i("Select homework item ${homework.id}") + view?.showTimetableDialog(homework) } private fun setBaseDateOnHolidays() { @@ -110,8 +106,6 @@ class HomeworkPresenter @Inject constructor( homeworkRepository.getHomework(student, semester, currentDate, currentDate, forceRefresh) } } - .delay(200, TimeUnit.MILLISECONDS) - .map { it.groupBy { homework -> homework.date }.toSortedMap() } .map { createHomeworkItem(it) } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) @@ -150,12 +144,12 @@ class HomeworkPresenter @Inject constructor( } } - private fun createHomeworkItem(items: Map>): List { - return items.flatMap { - HomeworkHeader(it.key).let { header -> - it.value.reversed().map { item -> HomeworkItem(header, item) } + private fun createHomeworkItem(items: List): List> { + return items.groupBy { it.date }.toSortedMap().map { (date, exams) -> + listOf(HomeworkItem(date, HomeworkItem.ViewType.HEADER)) + exams.reversed().map { exam -> + HomeworkItem(exam, HomeworkItem.ViewType.ITEM) } - } + }.flatten() } private fun reloadView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt index 1d241df4..2a678cd4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt @@ -9,7 +9,7 @@ interface HomeworkView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List>) fun clearData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt index 3706b56e..5d6ee162 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt @@ -1,14 +1,15 @@ package io.github.wulkanowy.ui.modules.homework.details import android.view.LayoutInflater -import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentBinding +import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentsHeaderBinding +import io.github.wulkanowy.databinding.ItemHomeworkDialogDetailsBinding import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.synthetic.main.item_homework_dialog_attachment.view.* -import kotlinx.android.synthetic.main.item_homework_dialog_details.view.* import javax.inject.Inject class HomeworkDetailsAdapter @Inject constructor() : @@ -30,6 +31,10 @@ class HomeworkDetailsAdapter @Inject constructor() : var onAttachmentClickListener: (url: String) -> Unit = {} + var onFullScreenClickListener = {} + + var onFullScreenExitClickListener = {} + override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0 override fun getItemViewType(position: Int) = when (position) { @@ -42,9 +47,9 @@ class HomeworkDetailsAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder(inflater.inflate(R.layout.item_homework_dialog_attachments_header, parent, false)) - ViewType.ATTACHMENT.id -> AttachmentViewHolder(inflater.inflate(R.layout.item_homework_dialog_attachment, parent, false)) - else -> DetailsViewHolder(inflater.inflate(R.layout.item_homework_dialog_details, parent, false)) + ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder(ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false)) + ViewType.ATTACHMENT.id -> AttachmentViewHolder(ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false)) + else -> DetailsViewHolder(ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false)) } } @@ -56,29 +61,42 @@ class HomeworkDetailsAdapter @Inject constructor() : } private fun bindDetailsViewHolder(holder: DetailsViewHolder) { - with(holder.view) { + with(holder.binding) { homeworkDialogDate.text = homework?.date?.toFormattedString() homeworkDialogEntryDate.text = homework?.entryDate?.toFormattedString() homeworkDialogSubject.text = homework?.subject homeworkDialogTeacher.text = homework?.teacher homeworkDialogContent.text = homework?.content + homeworkDialogFullScreen.setOnClickListener { + homeworkDialogFullScreen.visibility = GONE + homeworkDialogFullScreenExit.visibility = VISIBLE + onFullScreenClickListener() + } + homeworkDialogFullScreenExit.setOnClickListener { + homeworkDialogFullScreen.visibility = VISIBLE + homeworkDialogFullScreenExit.visibility = GONE + onFullScreenExitClickListener() + } } } private fun bindAttachmentViewHolder(holder: AttachmentViewHolder, position: Int) { val item = attachments[position] - with(holder.view) { + with(holder.binding) { homeworkDialogAttachment.text = item.second - setOnClickListener { + root.setOnClickListener { onAttachmentClickListener(item.first) } } } - class DetailsViewHolder(val view: View) : RecyclerView.ViewHolder(view) + class DetailsViewHolder(val binding: ItemHomeworkDialogDetailsBinding) : + RecyclerView.ViewHolder(binding.root) - class AttachmentsHeaderViewHolder(val view: View) : RecyclerView.ViewHolder(view) + class AttachmentsHeaderViewHolder(val binding: ItemHomeworkDialogAttachmentsHeaderBinding) : + RecyclerView.ViewHolder(binding.root) - class AttachmentViewHolder(val view: View) : RecyclerView.ViewHolder(view) + class AttachmentViewHolder(val binding: ItemHomeworkDialogAttachmentBinding) : + RecyclerView.ViewHolder(binding.root) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt index 54cb5c68..7b3b9821 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt @@ -5,16 +5,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.databinding.DialogHomeworkBinding import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment import io.github.wulkanowy.utils.openInternetBrowser -import kotlinx.android.synthetic.main.dialog_homework.* import javax.inject.Inject -class HomeworkDetailsDialog : BaseDialogFragment(), HomeworkDetailsView { +class HomeworkDetailsDialog : BaseDialogFragment(), HomeworkDetailsView { @Inject lateinit var presenter: HomeworkDetailsPresenter @@ -43,7 +45,7 @@ class HomeworkDetailsDialog : BaseDialogFragment(), HomeworkDetailsView { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_homework, container, false) + return DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -53,14 +55,18 @@ class HomeworkDetailsDialog : BaseDialogFragment(), HomeworkDetailsView { @SuppressLint("SetTextI18n") override fun initView() { - homeworkDialogRead.text = view?.context?.getString(if (homework.isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) - homeworkDialogRead.setOnClickListener { presenter.toggleDone(homework) } - homeworkDialogClose.setOnClickListener { dismiss() } + with(binding) { + homeworkDialogRead.text = view?.context?.getString(if (homework.isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) + homeworkDialogRead.setOnClickListener { presenter.toggleDone(homework) } + homeworkDialogClose.setOnClickListener { dismiss() } + } - with(homeworkDialogRecycler) { + with(binding.homeworkDialogRecycler) { layoutManager = LinearLayoutManager(context) adapter = detailsAdapter.apply { onAttachmentClickListener = { context.openInternetBrowser(it, ::showMessage) } + onFullScreenClickListener = { dialog?.window?.setLayout(MATCH_PARENT, MATCH_PARENT) } + onFullScreenExitClickListener = { dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) } homework = this@HomeworkDetailsDialog.homework } } @@ -68,7 +74,7 @@ class HomeworkDetailsDialog : BaseDialogFragment(), HomeworkDetailsView { override fun updateMarkAsDoneLabel(isDone: Boolean) { (parentFragment as? HomeworkFragment)?.onReloadList() - homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) + binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index 19f23593..ffb36fe6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -4,8 +4,8 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem -import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.databinding.ActivityLoginBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment @@ -14,10 +14,9 @@ import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment import io.github.wulkanowy.utils.setOnSelectPageListener -import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject -class LoginActivity : BaseActivity(), LoginView { +class LoginActivity : BaseActivity(), LoginView { @Inject override lateinit var presenter: LoginPresenter @@ -30,13 +29,13 @@ class LoginActivity : BaseActivity(), LoginView { fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) } - override val currentViewIndex get() = loginViewpager.currentItem + override val currentViewIndex get() = binding.loginViewpager.currentItem override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_login) - setSupportActionBar(loginToolbar) - messageContainer = loginContainer + setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) + setSupportActionBar(binding.loginToolbar) + messageContainer = binding.loginContainer presenter.onAttachView(this) } @@ -48,7 +47,7 @@ class LoginActivity : BaseActivity(), LoginView { } with(loginAdapter) { - containerId = loginViewpager.id + containerId = binding.loginViewpager.id addFragments(listOf( LoginFormFragment.newInstance(), LoginSymbolFragment.newInstance(), @@ -58,7 +57,7 @@ class LoginActivity : BaseActivity(), LoginView { )) } - with(loginViewpager) { + with(binding.loginViewpager) { offscreenPageLimit = 2 adapter = loginAdapter setOnSelectPageListener(presenter::onViewSelected) @@ -71,7 +70,7 @@ class LoginActivity : BaseActivity(), LoginView { } override fun switchView(index: Int) { - loginViewpager.setCurrentItem(index, false) + binding.loginViewpager.setCurrentItem(index, false) } override fun showActionBar(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index aaea31ec..3b6a985b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -1,15 +1,14 @@ package io.github.wulkanowy.ui.modules.login.advanced import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import android.widget.ArrayAdapter import androidx.core.widget.doOnTextChanged import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity @@ -17,10 +16,11 @@ import io.github.wulkanowy.ui.modules.login.form.LoginSymbolAdapter import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.setOnEditorDoneSignIn import io.github.wulkanowy.utils.showSoftInput -import kotlinx.android.synthetic.main.fragment_login_advanced.* import javax.inject.Inject -class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView { +class LoginAdvancedFragment : + BaseFragment(R.layout.fragment_login_advanced), + LoginAdvancedView { @Inject lateinit var presenter: LoginAdvancedPresenter @@ -30,17 +30,17 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView { } override val formLoginType: String - get() = when (loginTypeSwitch.checkedRadioButtonId) { + get() = when (binding.loginTypeSwitch.checkedRadioButtonId) { R.id.loginTypeApi -> "API" R.id.loginTypeScrapper -> "SCRAPPER" else -> "HYBRID" } override val formUsernameValue: String - get() = loginFormUsername.text.toString().trim() + get() = binding.loginFormUsername.text.toString().trim() override val formPassValue: String - get() = loginFormPass.text.toString().trim() + get() = binding.loginFormPass.text.toString().trim() private lateinit var hostKeys: Array @@ -49,19 +49,19 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView { private lateinit var hostSymbols: Array override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() override val formPinValue: String - get() = loginFormPin.text.toString().trim() + get() = binding.loginFormPin.text.toString().trim() override val formSymbolValue: String - get() = loginFormSymbol.text.toString().trim() + get() = binding.loginFormSymbol.text.toString().trim() override val formTokenValue: String - get() = loginFormToken.text.toString().trim() + get() = binding.loginFormToken.text.toString().trim() override val nicknameLabel: String get() = getString(R.string.login_nickname_hint) @@ -69,12 +69,9 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView { override val emailLabel: String get() = getString(R.string.login_email_hint) - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_login_advanced, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLoginAdvancedBinding.bind(view) presenter.onAttachView(this) } @@ -83,191 +80,201 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView { hostValues = resources.getStringArray(R.array.hosts_values) hostSymbols = resources.getStringArray(R.array.hosts_symbols) - loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() } - loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() } - loginFormPin.doOnTextChanged { _, _, _, _ -> presenter.onPinTextChanged() } - loginFormSymbol.doOnTextChanged { _, _, _, _ -> presenter.onSymbolTextChanged() } - loginFormToken.doOnTextChanged { _, _, _, _ -> presenter.onTokenTextChanged() } - loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } - loginFormSignIn.setOnClickListener { presenter.onSignInClick() } + with(binding) { + loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() } + loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() } + loginFormPin.doOnTextChanged { _, _, _, _ -> presenter.onPinTextChanged() } + loginFormSymbol.doOnTextChanged { _, _, _, _ -> presenter.onSymbolTextChanged() } + loginFormToken.doOnTextChanged { _, _, _, _ -> presenter.onTokenTextChanged() } + loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } + loginFormSignIn.setOnClickListener { presenter.onSignInClick() } - loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> - presenter.onLoginModeSelected(when (checkedId) { - R.id.loginTypeApi -> Sdk.Mode.API - R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER - else -> Sdk.Mode.HYBRID - }) + loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> + presenter.onLoginModeSelected(when (checkedId) { + R.id.loginTypeApi -> Sdk.Mode.API + R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER + else -> Sdk.Mode.HYBRID + }) + } + + loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } + loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } + + loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) } - loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } - loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } - - loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) - - with(loginFormHost) { + with(binding.loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) - setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() } + setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } } } override fun showMobileApiWarningMessage() { - loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api) + binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api) } override fun showScraperWarningMessage() { - loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper) + binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper) } override fun showHybridWarningMessage() { - loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid) + binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid) } override fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String) { - loginFormUsername.setText(username) - loginFormPass.setText(pass) - loginFormToken.setText(token) - loginFormSymbol.setText(symbol) - loginFormPin.setText(pin) + with(binding) { + loginFormUsername.setText(username) + loginFormPass.setText(pass) + loginFormToken.setText(token) + loginFormSymbol.setText(symbol) + loginFormPin.setText(pin) + } } override fun setUsernameLabel(label: String) { - loginFormUsernameLayout.hint = label + binding.loginFormUsernameLayout.hint = label } override fun setSymbol(symbol: String) { - loginFormSymbol.setText(symbol) + binding.loginFormSymbol.setText(symbol) } override fun setErrorUsernameRequired() { - with(loginFormUsernameLayout) { + with(binding.loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorLoginRequired() { - with(loginFormUsernameLayout) { + with(binding.loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_invalid_login) } } override fun setErrorEmailRequired() { - with(loginFormUsernameLayout) { + with(binding.loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_invalid_email) } } override fun setErrorPassRequired(focus: Boolean) { - with(loginFormPassLayout) { + with(binding.loginFormPassLayout) { if (focus) requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorPassInvalid(focus: Boolean) { - with(loginFormPassLayout) { + with(binding.loginFormPassLayout) { if (focus) requestFocus() error = getString(R.string.login_invalid_password) } } override fun setErrorPassIncorrect() { - with(loginFormPassLayout) { + with(binding.loginFormPassLayout) { requestFocus() error = getString(R.string.login_incorrect_password) } } override fun setErrorPinRequired() { - with(loginFormPinLayout) { + with(binding.loginFormPinLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorPinInvalid(message: String) { - with(loginFormPinLayout) { + with(binding.loginFormPinLayout) { requestFocus() error = message } } override fun setErrorSymbolRequired() { - with(loginFormSymbolLayout) { + with(binding.loginFormSymbolLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorSymbolInvalid(message: String) { - with(loginFormSymbolLayout) { + with(binding.loginFormSymbolLayout) { requestFocus() error = message } } override fun setErrorTokenRequired() { - with(loginFormTokenLayout) { + with(binding.loginFormTokenLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorTokenInvalid(message: String) { - with(loginFormTokenLayout) { + with(binding.loginFormTokenLayout) { requestFocus() error = message } } override fun clearUsernameError() { - loginFormUsernameLayout.error = null + binding.loginFormUsernameLayout.error = null } override fun clearPassError() { - loginFormPassLayout.error = null + binding.loginFormPassLayout.error = null } override fun clearPinKeyError() { - loginFormPinLayout.error = null + binding.loginFormPinLayout.error = null } override fun clearSymbolError() { - loginFormSymbolLayout.error = null + binding.loginFormSymbolLayout.error = null } override fun clearTokenError() { - loginFormTokenLayout.error = null + binding.loginFormTokenLayout.error = null } override fun showOnlyHybridModeInputs() { - loginFormUsernameLayout.visibility = VISIBLE - loginFormPassLayout.visibility = VISIBLE - loginFormHostLayout.visibility = VISIBLE - loginFormPinLayout.visibility = GONE - loginFormSymbolLayout.visibility = VISIBLE - loginFormTokenLayout.visibility = GONE + with(binding) { + loginFormUsernameLayout.visibility = VISIBLE + loginFormPassLayout.visibility = VISIBLE + loginFormHostLayout.visibility = VISIBLE + loginFormPinLayout.visibility = GONE + loginFormSymbolLayout.visibility = VISIBLE + loginFormTokenLayout.visibility = GONE + } } override fun showOnlyScrapperModeInputs() { - loginFormUsernameLayout.visibility = VISIBLE - loginFormPassLayout.visibility = VISIBLE - loginFormHostLayout.visibility = VISIBLE - loginFormPinLayout.visibility = GONE - loginFormSymbolLayout.visibility = VISIBLE - loginFormTokenLayout.visibility = GONE + with(binding) { + loginFormUsernameLayout.visibility = VISIBLE + loginFormPassLayout.visibility = VISIBLE + loginFormHostLayout.visibility = VISIBLE + loginFormPinLayout.visibility = GONE + loginFormSymbolLayout.visibility = VISIBLE + loginFormTokenLayout.visibility = GONE + } } override fun showOnlyMobileApiModeInputs() { - loginFormUsernameLayout.visibility = GONE - loginFormPassLayout.visibility = GONE - loginFormHostLayout.visibility = GONE - loginFormPinLayout.visibility = VISIBLE - loginFormSymbolLayout.visibility = VISIBLE - loginFormTokenLayout.visibility = VISIBLE + with(binding) { + loginFormUsernameLayout.visibility = GONE + loginFormPassLayout.visibility = GONE + loginFormHostLayout.visibility = GONE + loginFormPinLayout.visibility = VISIBLE + loginFormSymbolLayout.visibility = VISIBLE + loginFormTokenLayout.visibility = VISIBLE + } } override fun showSoftKeyboard() { @@ -279,17 +286,17 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView { } override fun showProgress(show: Boolean) { - loginFormProgress.visibility = if (show) VISIBLE else GONE + binding.loginFormProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - loginFormContainer.visibility = if (show) VISIBLE else GONE + binding.loginFormContainer.visibility = if (show) VISIBLE else GONE } override fun notifyParentAccountLogged(students: List) { (activity as? LoginActivity)?.onFormFragmentAccountLogged(students, Triple( - loginFormUsername.text.toString(), - loginFormPass.text.toString(), + binding.loginFormUsername.text.toString(), + binding.loginFormPass.text.toString(), resources.getStringArray(R.array.hosts_values)[1] )) } @@ -300,7 +307,7 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView { } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 058e702e..a2a083c0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -2,14 +2,13 @@ package io.github.wulkanowy.ui.modules.login.form import android.annotation.SuppressLint import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import androidx.core.widget.doOnTextChanged import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.utils.AppInfo @@ -18,10 +17,10 @@ import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.setOnEditorDoneSignIn import io.github.wulkanowy.utils.showSoftInput -import kotlinx.android.synthetic.main.fragment_login_form.* import javax.inject.Inject -class LoginFormFragment : BaseFragment(), LoginFormView { +class LoginFormFragment : BaseFragment(R.layout.fragment_login_form), + LoginFormView { @Inject lateinit var presenter: LoginFormPresenter @@ -34,16 +33,16 @@ class LoginFormFragment : BaseFragment(), LoginFormView { } override val formUsernameValue: String - get() = loginFormUsername.text.toString() + get() = binding.loginFormUsername.text.toString() override val formPassValue: String - get() = loginFormPass.text.toString() + get() = binding.loginFormPass.text.toString() override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() override val nicknameLabel: String get() = getString(R.string.login_nickname_hint) @@ -57,12 +56,9 @@ class LoginFormFragment : BaseFragment(), LoginFormView { private lateinit var hostSymbols: Array - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_login_form, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLoginFormBinding.bind(view) presenter.onAttachView(this) } @@ -71,81 +67,85 @@ class LoginFormFragment : BaseFragment(), LoginFormView { hostValues = resources.getStringArray(R.array.hosts_values) hostSymbols = resources.getStringArray(R.array.hosts_symbols) - loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() } - loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() } - loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } - loginFormSignIn.setOnClickListener { presenter.onSignInClick() } - loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() } - loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() } - loginFormFaq.setOnClickListener { presenter.onFaqClick() } - loginFormContactEmail.setOnClickListener { presenter.onEmailClick() } - loginFormRecoverLink.setOnClickListener { presenter.onRecoverClick() } - loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } + with(binding) { + loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() } + loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() } + loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } + loginFormSignIn.setOnClickListener { presenter.onSignInClick() } + loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() } + loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() } + loginFormFaq.setOnClickListener { presenter.onFaqClick() } + loginFormContactEmail.setOnClickListener { presenter.onEmailClick() } + loginFormRecoverLink.setOnClickListener { presenter.onRecoverClick() } + loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } + } - with(loginFormHost) { + with(binding.loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) - setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() } + setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } } } override fun setCredentials(username: String, pass: String) { - loginFormUsername.setText(username) - loginFormPass.setText(pass) + with(binding) { + loginFormUsername.setText(username) + loginFormPass.setText(pass) + } } override fun setUsernameLabel(label: String) { - loginFormUsernameLayout.hint = label + binding.loginFormUsernameLayout.hint = label } override fun setErrorUsernameRequired() { - with(loginFormUsernameLayout) { + with(binding.loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorLoginRequired() { - with(loginFormUsernameLayout) { + with(binding.loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_invalid_login) } } override fun setErrorEmailRequired() { - with(loginFormUsernameLayout) { + with(binding.loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_invalid_email) } } override fun setErrorPassRequired(focus: Boolean) { - with(loginFormPassLayout) { + with(binding.loginFormPassLayout) { if (focus) requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorPassInvalid(focus: Boolean) { - with(loginFormPassLayout) { + with(binding.loginFormPassLayout) { if (focus) requestFocus() error = getString(R.string.login_invalid_password) } } override fun setErrorPassIncorrect() { - with(loginFormPassLayout) { + with(binding.loginFormPassLayout) { requestFocus() error = getString(R.string.login_incorrect_password) } } override fun clearUsernameError() { - loginFormUsernameLayout.error = null + binding.loginFormUsernameLayout.error = null } override fun clearPassError() { - loginFormPassLayout.error = null + binding.loginFormPassLayout.error = null } override fun showSoftKeyboard() { @@ -157,16 +157,16 @@ class LoginFormFragment : BaseFragment(), LoginFormView { } override fun showProgress(show: Boolean) { - loginFormProgress.visibility = if (show) VISIBLE else GONE + binding.loginFormProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - loginFormContainer.visibility = if (show) VISIBLE else GONE + binding.loginFormContainer.visibility = if (show) VISIBLE else GONE } @SuppressLint("SetTextI18n") override fun showVersion() { - loginFormVersion.text = "v${appInfo.versionName}" + binding.loginFormVersion.text = "v${appInfo.versionName}" } override fun notifyParentAccountLogged(students: List, loginData: Triple) { @@ -178,7 +178,7 @@ class LoginFormFragment : BaseFragment(), LoginFormView { } override fun showContact(show: Boolean) { - loginFormContact.visibility = if (show) VISIBLE else GONE + binding.loginFormContact.visibility = if (show) VISIBLE else GONE } override fun openAdvancedLogin() { @@ -190,8 +190,8 @@ class LoginFormFragment : BaseFragment(), LoginFormView { } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } override fun openFaqPage() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt index 1bff4926..97e45be9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt @@ -3,25 +3,24 @@ package io.github.wulkanowy.ui.modules.login.recover import android.annotation.SuppressLint import android.graphics.Color import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import android.webkit.JavascriptInterface import android.webkit.WebView import android.webkit.WebViewClient import androidx.core.widget.doOnTextChanged import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentLoginRecoverBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.form.LoginSymbolAdapter import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.showSoftInput -import kotlinx.android.synthetic.main.fragment_login_recover.* import javax.inject.Inject -class LoginRecoverFragment : BaseFragment(), LoginRecoverView { +class LoginRecoverFragment : + BaseFragment(R.layout.fragment_login_recover), LoginRecoverView { @Inject lateinit var presenter: LoginRecoverPresenter @@ -37,13 +36,13 @@ class LoginRecoverFragment : BaseFragment(), LoginRecoverView { private lateinit var hostSymbols: Array override val recoverHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(loginRecoverHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginRecoverHost.text.toString())).orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(loginRecoverHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginRecoverHost.text.toString())).orEmpty() override val recoverNameValue: String - get() = loginRecoverName.text.toString().trim() + get() = binding.loginRecoverName.text.toString().trim() override val emailHintString: String get() = getString(R.string.login_email_hint) @@ -54,91 +53,90 @@ class LoginRecoverFragment : BaseFragment(), LoginRecoverView { override val invalidEmailString: String get() = getString(R.string.login_invalid_email) - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_login_recover, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLoginRecoverBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - loginRecoverWebView.setBackgroundColor(Color.TRANSPARENT) hostKeys = resources.getStringArray(R.array.hosts_keys) hostValues = resources.getStringArray(R.array.hosts_values) hostSymbols = resources.getStringArray(R.array.hosts_symbols) - loginRecoverName.doOnTextChanged { _, _, _, _ -> presenter.onNameTextChanged() } - loginRecoverHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } - loginRecoverButton.setOnClickListener { presenter.onRecoverClick() } - loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() } - loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() } - loginRecoverLogin.setOnClickListener { (activity as LoginActivity).switchView(0) } + with(binding) { + loginRecoverWebView.setBackgroundColor(Color.TRANSPARENT) + loginRecoverName.doOnTextChanged { _, _, _, _ -> presenter.onNameTextChanged() } + loginRecoverHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } + loginRecoverButton.setOnClickListener { presenter.onRecoverClick() } + loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() } + loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() } + loginRecoverLogin.setOnClickListener { (activity as LoginActivity).switchView(0) } + } - with(loginRecoverHost) { + with(binding.loginRecoverHost) { setText(hostKeys.getOrNull(0).orEmpty()) setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) - setOnClickListener { if (loginRecoverFormContainer.visibility == GONE) dismissDropDown() } + setOnClickListener { if (binding.loginRecoverFormContainer.visibility == GONE) dismissDropDown() } } } override fun setDefaultCredentials(username: String) { - loginRecoverName.setText(username) + binding.loginRecoverName.setText(username) } override fun setErrorNameRequired() { - with(loginRecoverNameLayout) { + with(binding.loginRecoverNameLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setUsernameHint(hint: String) { - loginRecoverNameLayout.hint = hint + binding.loginRecoverNameLayout.hint = hint } override fun setUsernameError(message: String) { - with(loginRecoverNameLayout) { + with(binding.loginRecoverNameLayout) { requestFocus() error = message } } override fun clearUsernameError() { - loginRecoverNameLayout.error = null + binding.loginRecoverNameLayout.error = null } override fun showProgress(show: Boolean) { - loginRecoverProgress.visibility = if (show) VISIBLE else GONE + binding.loginRecoverProgress.visibility = if (show) VISIBLE else GONE } override fun showRecoverForm(show: Boolean) { - loginRecoverFormContainer.visibility = if (show) VISIBLE else GONE + binding.loginRecoverFormContainer.visibility = if (show) VISIBLE else GONE } override fun showCaptcha(show: Boolean) { - loginRecoverCaptchaContainer.visibility = if (show) VISIBLE else GONE + binding.loginRecoverCaptchaContainer.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - loginRecoverError.visibility = if (show) VISIBLE else GONE + binding.loginRecoverError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - loginRecoverErrorMessage.text = message + binding.loginRecoverErrorMessage.text = message } override fun showSuccessView(show: Boolean) { - loginRecoverSuccess.visibility = if (show) VISIBLE else GONE + binding.loginRecoverSuccess.visibility = if (show) VISIBLE else GONE } override fun setSuccessTitle(title: String) { - loginRecoverSuccessTitle.text = title + binding.loginRecoverSuccessTitle.text = title } override fun setSuccessMessage(message: String) { - loginRecoverSuccessMessage.text = message + binding.loginRecoverSuccessMessage.text = message } override fun showSoftKeyboard() { @@ -159,7 +157,7 @@ class LoginRecoverFragment : BaseFragment(), LoginRecoverView { callback:e =>Android.captchaCallback(e)}) """.trimIndent() - with(loginRecoverWebView) { + with(binding.loginRecoverWebView) { settings.javaScriptEnabled = true webViewClient = object : WebViewClient() { private var recoverWebViewSuccess: Boolean = true @@ -181,6 +179,8 @@ class LoginRecoverFragment : BaseFragment(), LoginRecoverView { loadDataWithBaseURL(url, html, "text/html", "UTF-8", null) addJavascriptInterface(object { + + @Suppress("UNUSED") @JavascriptInterface fun captchaCallback(reCaptchaResponse: String) { activity?.runOnUiThread { @@ -197,8 +197,8 @@ class LoginRecoverFragment : BaseFragment(), LoginRecoverView { } override fun onDestroyView() { - super.onDestroyView() - loginRecoverWebView.destroy() presenter.onDetachView() + + super.onDestroyView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt new file mode 100644 index 00000000..7383a5ec --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt @@ -0,0 +1,51 @@ +package io.github.wulkanowy.ui.modules.login.studentselect + +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.Student +import io.github.wulkanowy.databinding.ItemLoginStudentSelectBinding +import javax.inject.Inject + +class LoginStudentSelectAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = emptyList>() + + var onClickListener: (Student, alreadySaved: Boolean) -> Unit = { _, _ -> } + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemLoginStudentSelectBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val (student, alreadySaved) = items[position] + + with(holder.binding) { + loginItemName.text = "${student.studentName} ${student.className}" + loginItemSchool.text = student.schoolName + loginItemName.isEnabled = !alreadySaved + loginItemSchool.isEnabled = !alreadySaved + loginItemCheck.isEnabled = !alreadySaved + loginItemSignedIn.visibility = if (alreadySaved) View.VISIBLE else View.GONE + + root.setOnClickListener { + onClickListener(student, alreadySaved) + + with(loginItemCheck) { + if (isEnabled) { + isChecked = !isChecked + } + } + } + } + } + + class ItemViewHolder(val binding: ItemLoginStudentSelectBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 9860af22..b0df5199 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -1,33 +1,30 @@ package io.github.wulkanowy.ui.modules.login.studentselect import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_login_student_select.* import java.io.Serializable import javax.inject.Inject -class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView { +class LoginStudentSelectFragment : + BaseFragment(R.layout.fragment_login_student_select), + LoginStudentSelectView { @Inject lateinit var presenter: LoginStudentSelectPresenter @Inject - lateinit var loginAdapter: FlexibleAdapter> + lateinit var loginAdapter: LoginStudentSelectAdapter @Inject lateinit var appInfo: AppInfo @@ -38,29 +35,32 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView { fun newInstance() = LoginStudentSelectFragment() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_login_student_select, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLoginStudentSelectBinding.bind(view) presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_STUDENTS)) } override fun initView() { - loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() } - loginAdapter.apply { setOnItemClickListener { presenter.onItemSelected(it) } } - loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() } - loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() } + loginAdapter.onClickListener = presenter::onItemSelected - loginStudentSelectRecycler.apply { - adapter = loginAdapter - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding) { + loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() } + loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() } + loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() } + + with(loginStudentSelectRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = loginAdapter + } } } - override fun updateData(data: List) { - loginAdapter.updateDataSet(data) + override fun updateData(data: List>) { + with(loginAdapter) { + items = data + notifyDataSetChanged() + } } override fun openMainView() { @@ -68,15 +68,15 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView { } override fun showProgress(show: Boolean) { - loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE + binding.loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - loginStudentSelectContent.visibility = if (show) VISIBLE else GONE + binding.loginStudentSelectContent.visibility = if (show) VISIBLE else GONE } override fun enableSignIn(enable: Boolean) { - loginStudentSelectSignIn.isEnabled = enable + binding.loginStudentSelectSignIn.isEnabled = enable } fun onParentInitStudentSelectFragment(students: List) { @@ -89,7 +89,7 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView { } override fun showContact(show: Boolean) { - loginStudentSelectContact.visibility = if (show) VISIBLE else GONE + binding.loginStudentSelectContact.visibility = if (show) VISIBLE else GONE } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectItem.kt deleted file mode 100644 index 06be6138..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectItem.kt +++ /dev/null @@ -1,71 +0,0 @@ -package io.github.wulkanowy.ui.modules.login.studentselect - -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.Student -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_login_student_select.* - -class LoginStudentSelectItem(val student: Student, val alreadySaved: Boolean) : - AbstractFlexibleItem() { - - override fun getLayoutRes() = R.layout.item_login_student_select - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ItemViewHolder { - return ItemViewHolder(view, adapter) - } - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ItemViewHolder, position: Int, payloads: MutableList) { - holder.apply { - loginItemName.text = "${student.studentName} ${student.className}" - loginItemSchool.text = student.schoolName - loginItemName.isEnabled = !alreadySaved - loginItemSchool.isEnabled = !alreadySaved - loginItemCheck.isEnabled = !alreadySaved - loginItemSignedIn.visibility = if (alreadySaved) VISIBLE else GONE - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as LoginStudentSelectItem - - if (student != other.student) return false - if (alreadySaved != other.alreadySaved) return false - - return true - } - - override fun hashCode(): Int { - return student.hashCode() - } - - class ItemViewHolder(view: View, val adapter: FlexibleAdapter<*>) : - FlexibleViewHolder(view, adapter), LayoutContainer { - - override val containerView: View - get() = itemView - - init { - loginItemCheck.keyListener = null - } - - override fun onClick(view: View?) { - super.onClick(view) - - if (loginItemCheck.isEnabled) { - loginItemCheck.apply { isChecked = !isChecked } - } - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index 82de5a88..b2d0aed9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.login.studentselect -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.ui.base.BasePresenter @@ -51,13 +50,14 @@ class LoginStudentSelectPresenter @Inject constructor( if (students.size == 1) registerStudents(students) } - fun onItemSelected(item: AbstractFlexibleItem<*>?) { - if (item is LoginStudentSelectItem && !item.alreadySaved) { - selectedStudents.removeAll { it == item.student } - .let { if (!it) selectedStudents.add(item.student) } + fun onItemSelected(student: Student, alreadySaved: Boolean) { + if (alreadySaved) return - view?.enableSignIn(selectedStudents.isNotEmpty()) - } + selectedStudents + .removeAll { it == student } + .let { if (!it) selectedStudents.add(student) } + + view?.enableSignIn(selectedStudents.isNotEmpty()) } private fun compareStudents(a: Student, b: Student): Boolean { @@ -73,19 +73,17 @@ class LoginStudentSelectPresenter @Inject constructor( disposable.add(studentRepository.getSavedStudents() .map { savedStudents -> students.map { student -> - Pair(student, savedStudents.any { compareStudents(student, it) }) + student to savedStudents.any { compareStudents(student, it) } } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .subscribe({ - view?.updateData(it.map { studentPair -> - LoginStudentSelectItem(studentPair.first, studentPair.second) - }) + view?.updateData(it) }, { errorHandler.dispatch(it) lastError = it - view?.updateData(students.map { student -> LoginStudentSelectItem(student, false) }) + view?.updateData(students.map { student -> student to false }) }) ) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt index d80b059d..89431dfc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.login.studentselect +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.ui.base.BaseView interface LoginStudentSelectView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List>) fun openMainView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt index 5602b624..befbffd5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt @@ -1,17 +1,16 @@ package io.github.wulkanowy.ui.modules.login.symbol import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.inputmethod.EditorInfo.IME_NULL import android.widget.ArrayAdapter import androidx.core.widget.doOnTextChanged import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.utils.AppInfo @@ -19,10 +18,10 @@ import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.showSoftInput -import kotlinx.android.synthetic.main.fragment_login_symbol.* import javax.inject.Inject -class LoginSymbolFragment : BaseFragment(), LoginSymbolView { +class LoginSymbolFragment : + BaseFragment(R.layout.fragment_login_symbol), LoginSymbolView { @Inject lateinit var presenter: LoginSymbolPresenter @@ -37,29 +36,28 @@ class LoginSymbolFragment : BaseFragment(), LoginSymbolView { } override val symbolNameError: CharSequence? - get() = loginSymbolNameLayout.error + get() = binding.loginSymbolNameLayout.error - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_login_symbol, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLoginSymbolBinding.bind(view) presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_LOGIN_DATA)) } override fun initView() { - loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) } - loginSymbolFaq.setOnClickListener { presenter.onFaqClick() } - loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() } + with(binding) { + loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) } + loginSymbolFaq.setOnClickListener { presenter.onFaqClick() } + loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() } - loginSymbolName.doOnTextChanged { _, _, _, _ -> presenter.onSymbolTextChanged() } + loginSymbolName.doOnTextChanged { _, _, _, _ -> presenter.onSymbolTextChanged() } - loginSymbolName.apply { - setOnEditorActionListener { _, id, _ -> - if (id == IME_ACTION_DONE || id == IME_NULL) loginSymbolSignIn.callOnClick() else false + loginSymbolName.apply { + setOnEditorActionListener { _, id, _ -> + if (id == IME_ACTION_DONE || id == IME_NULL) loginSymbolSignIn.callOnClick() else false + } + setAdapter(ArrayAdapter(context, android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) } - setAdapter(ArrayAdapter(context, android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) } } @@ -68,25 +66,25 @@ class LoginSymbolFragment : BaseFragment(), LoginSymbolView { } override fun setErrorSymbolIncorrect() { - loginSymbolNameLayout.apply { + binding.loginSymbolNameLayout.apply { requestFocus() error = getString(R.string.login_incorrect_symbol) } } override fun setErrorSymbolRequire() { - loginSymbolNameLayout.apply { + binding.loginSymbolNameLayout.apply { requestFocus() error = getString(R.string.login_field_required) } } override fun clearSymbolError() { - loginSymbolNameLayout.error = null + binding.loginSymbolNameLayout.error = null } override fun clearAndFocusSymbol() { - loginSymbolNameLayout.apply { + binding.loginSymbolNameLayout.apply { editText?.text = null requestFocus() } @@ -101,11 +99,11 @@ class LoginSymbolFragment : BaseFragment(), LoginSymbolView { } override fun showProgress(show: Boolean) { - loginSymbolProgress.visibility = if (show) VISIBLE else GONE + binding.loginSymbolProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - loginSymbolContainer.visibility = if (show) VISIBLE else GONE + binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE } override fun notifyParentAccountLogged(students: List) { @@ -118,12 +116,12 @@ class LoginSymbolFragment : BaseFragment(), LoginSymbolView { } override fun showContact(show: Boolean) { - loginSymbolContact.visibility = if (show) VISIBLE else GONE + binding.loginSymbolContact.visibility = if (show) VISIBLE else GONE } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } override fun openFaqPage() { @@ -139,7 +137,7 @@ class LoginSymbolFragment : BaseFragment(), LoginSymbolView { "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName, - "$host/${loginSymbolName.text}", + "$host/${binding.loginSymbolName.text}", lastError ) ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt index 12bf1a13..0775ce18 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt @@ -1,19 +1,19 @@ package io.github.wulkanowy.ui.modules.luckynumber import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.LuckyNumber +import io.github.wulkanowy.databinding.FragmentLuckyNumberBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainView -import kotlinx.android.synthetic.main.fragment_lucky_number.* import javax.inject.Inject -class LuckyNumberFragment : BaseFragment(), LuckyNumberView, MainView.TitledView { +class LuckyNumberFragment : + BaseFragment(R.layout.fragment_lucky_number), LuckyNumberView, + MainView.TitledView { @Inject lateinit var presenter: LuckyNumberPresenter @@ -25,58 +25,57 @@ class LuckyNumberFragment : BaseFragment(), LuckyNumberView, MainView.TitledView override val titleStringId: Int get() = R.string.lucky_number_title - override val isViewEmpty get() = luckyNumberText.text.isBlank() + override val isViewEmpty get() = binding.luckyNumberText.text.isBlank() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_lucky_number, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = luckyNumberSwipe + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLuckyNumberBinding.bind(view) + messageContainer = binding.luckyNumberSwipe presenter.onAttachView(this) } override fun initView() { - luckyNumberSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() } - luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + luckyNumberSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() } + luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() } + } } override fun updateData(data: LuckyNumber) { - luckyNumberText.text = data.luckyNumber.toString() + binding.luckyNumberText.text = data.luckyNumber.toString() } override fun hideRefresh() { - luckyNumberSwipe.isRefreshing = false + binding.luckyNumberSwipe.isRefreshing = false } override fun showEmpty(show: Boolean) { - luckyNumberEmpty.visibility = if (show) VISIBLE else GONE + binding.luckyNumberEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - luckyNumberError.visibility = if (show) VISIBLE else GONE + binding.luckyNumberError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - luckyNumberErrorMessage.text = message + binding.luckyNumberErrorMessage.text = message } override fun showProgress(show: Boolean) { - luckyNumberProgress.visibility = if (show) VISIBLE else GONE + binding.luckyNumberProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - luckyNumberSwipe.isEnabled = enable + binding.luckyNumberSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - luckyNumberContent.visibility = if (show) VISIBLE else GONE + binding.luckyNumberContent.visibility = if (show) VISIBLE else GONE } override fun onDestroyView() { - super.onDestroyView() presenter.onDetachView() + super.onDestroyView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt index e8ce3bcf..961ac633 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt @@ -4,34 +4,39 @@ import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS import android.content.Intent +import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AlertDialog -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.databinding.ActivityWidgetConfigureBinding import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.base.WidgetConfigureAdapter import io.github.wulkanowy.ui.modules.login.LoginActivity -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.activity_widget_configure.* +import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject -class LuckyNumberWidgetConfigureActivity : BaseActivity(), +class LuckyNumberWidgetConfigureActivity : + BaseActivity(), LuckyNumberWidgetConfigureView { @Inject - lateinit var configureAdapter: FlexibleAdapter> + lateinit var configureAdapter: WidgetConfigureAdapter @Inject override lateinit var presenter: LuckyNumberWidgetConfigurePresenter + @Inject + lateinit var appInfo: AppInfo + private var dialog: AlertDialog? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setResult(RESULT_CANCELED) - setContentView(R.layout.activity_widget_configure) + setContentView(ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root) intent.extras.let { presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID)) @@ -39,31 +44,35 @@ class LuckyNumberWidgetConfigureActivity : BaseActivity= Build.VERSION_CODES.Q) items += (getString(R.string.widget_timetable_theme_system)) - dialog = AlertDialog.Builder(this, R.style.WulkanowyTheme_WidgetAccountSwitcher) + dialog = AlertDialog.Builder(this, R.style.WulkanowyTheme_WidgetAccountSwitcher) .setTitle(R.string.widget_timetable_theme_title) - .setOnDismissListener { presenter.onDismissThemeView() } + .setOnDismissListener { presenter.onDismissThemeView() } .setSingleChoiceItems(items, -1) { _, which -> presenter.onThemeSelect(which) } .show() } - override fun updateData(data: List) { - configureAdapter.updateDataSet(data) + override fun updateData(data: List>) { + with(configureAdapter) { + items = data + notifyDataSetChanged() + } } override fun updateLuckyNumberWidget(widgetId: Int) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureItem.kt deleted file mode 100644 index e260b7fc..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureItem.kt +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.wulkanowy.ui.modules.luckynumberwidget - -import android.annotation.SuppressLint -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.ui.modules.timetablewidget.TimetableWidgetConfigureItem -import io.github.wulkanowy.utils.getThemeAttrColor -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_account.* - -class LuckyNumberWidgetConfigureItem(var student: Student, val isCurrent: Boolean) : - AbstractFlexibleItem() { - - override fun getLayoutRes() = R.layout.item_account - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>) = ViewHolder(view, adapter) - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList) { - val context = holder.contentView.context - - val colorImage = if (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) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TimetableWidgetConfigureItem - - 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 - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt index 6e4716bf..468f9b57 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.student.StudentRepository @@ -29,11 +28,9 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( loadData() } - fun onItemSelect(item: AbstractFlexibleItem<*>) { - if (item is LuckyNumberWidgetConfigureItem) { - selectedStudent = item.student - view?.showThemeDialog() - } + fun onItemSelect(student: Student) { + selectedStudent = student + view?.showThemeDialog() } fun onThemeSelect(index: Int) { @@ -43,7 +40,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( registerStudent(selectedStudent) } - fun onDismissThemeView(){ + fun onDismissThemeView() { view?.finishView() } @@ -51,7 +48,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( disposable.add(studentRepository.getSavedStudents(false) .map { it to appWidgetId?.let { id -> sharedPref.getLong(getStudentWidgetKey(id), 0) } } .map { (students, currentStudentId) -> - students.map { student -> LuckyNumberWidgetConfigureItem(student, student.id == currentStudentId) } + students.map { student -> student to (student.id == currentStudentId) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) @@ -59,7 +56,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( when { it.isEmpty() -> view?.openLoginView() it.size == 1 -> { - selectedStudent = it.single().student + selectedStudent = it.single().first view?.showThemeDialog() } else -> view?.updateData(it) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt index fa4c0cc6..c8c348ed 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.ui.base.BaseView interface LuckyNumberWidgetConfigureView : BaseView { @@ -8,7 +9,7 @@ interface LuckyNumberWidgetConfigureView : BaseView { fun showThemeDialog() - fun updateData(data: List) + fun updateData(data: List>) fun updateLuckyNumberWidget(widgetId: Int) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index e4b1072b..55a048b3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -8,6 +8,7 @@ import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.os.Bundle import android.view.View.GONE import android.view.View.VISIBLE @@ -62,14 +63,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) { super.onUpdate(context, appWidgetManager, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> - val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val layoutId = if (savedTheme == 0L) R.layout.widget_luckynumber else R.layout.widget_luckynumber_dark val luckyNumber = getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) val appIntent = PendingIntent.getActivity(context, MainView.Section.LUCKY_NUMBER.id, MainActivity.getStartIntent(context, MainView.Section.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT) - val remoteView = RemoteViews(context.packageName, layoutId).apply { + val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)).apply { setTextViewText(R.id.luckyNumberWidgetNumber, luckyNumber?.luckyNumber?.toString() ?: "#") setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) } @@ -82,17 +81,19 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { override fun onDeleted(context: Context?, appWidgetIds: IntArray?) { super.onDeleted(context, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> - if (appWidgetId != 0) sharedPref.delete(getStudentWidgetKey(appWidgetId)) + with(sharedPref) { + delete(getHeightWidgetKey(appWidgetId)) + delete(getStudentWidgetKey(appWidgetId)) + delete(getThemeWidgetKey(appWidgetId)) + delete(getWidthWidgetKey(appWidgetId)) + } } } override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) - val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val layoutId = if (savedTheme == 0L) R.layout.widget_luckynumber else R.layout.widget_luckynumber_dark - - val remoteView = RemoteViews(context.packageName, layoutId) + val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) setStyles(remoteView, appWidgetId, newOptions) appWidgetManager.updateAppWidget(appWidgetId, remoteView) @@ -102,8 +103,10 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(getWidthWidgetKey(appWidgetId), 74).toInt() val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(getHeightWidgetKey(appWidgetId), 74).toInt() - sharedPref.putLong(getWidthWidgetKey(appWidgetId), width.toLong()) - sharedPref.putLong(getHeightWidgetKey(appWidgetId), height.toLong()) + with(sharedPref) { + putLong(getWidthWidgetKey(appWidgetId), width.toLong()) + putLong(getHeightWidgetKey(appWidgetId), height.toLong()) + } val rows = getCellsForSize(height) val cols = getCellsForSize(width) @@ -162,4 +165,15 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { null } } + + private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int { + val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) + val isSystemDarkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + + return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) { + R.layout.widget_luckynumber_dark + } else { + R.layout.widget_luckynumber + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 864ad423..9ce19079 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -9,7 +9,6 @@ import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Bundle import android.view.Menu import android.view.MenuItem -import androidx.core.graphics.ColorUtils import androidx.core.view.ViewCompat import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment @@ -20,6 +19,7 @@ import com.ncapdevi.fragnav.FragNavController import com.ncapdevi.fragnav.FragNavController.Companion.HIDE import dagger.Lazy import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.ActivityMainBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.account.AccountDialog import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment @@ -35,11 +35,10 @@ import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.safelyPopFragments import io.github.wulkanowy.utils.setOnViewChangeListener -import kotlinx.android.synthetic.main.activity_main.* import timber.log.Timber import javax.inject.Inject -class MainActivity : BaseActivity(), MainView { +class MainActivity : BaseActivity(), MainView { @Inject override lateinit var presenter: MainPresenter @@ -83,9 +82,9 @@ class MainActivity : BaseActivity(), MainView { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - setSupportActionBar(mainToolbar) - messageContainer = mainFragmentContainer + setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) + setSupportActionBar(binding.mainToolbar) + messageContainer = binding.mainFragmentContainer presenter.onAttachView(this, intent.getSerializableExtra(EXTRA_START_MENU) as? MainView.Section) @@ -101,12 +100,12 @@ class MainActivity : BaseActivity(), MainView { } override fun initView() { - with(mainToolbar) { + with(binding.mainToolbar) { if (SDK_INT >= LOLLIPOP) stateListAnimator = null setBackgroundColor(overlayProvider.get().compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f))) } - with(mainBottomNav) { + with(binding.mainBottomNav) { addItems(listOf( AHBottomNavigationItem(R.string.grade_title, R.drawable.ic_main_grade, 0), AHBottomNavigationItem(R.string.attendance_title, R.drawable.ic_main_attendance, 0), @@ -115,7 +114,7 @@ class MainActivity : BaseActivity(), MainView { AHBottomNavigationItem(R.string.more_title, R.drawable.ic_main_more, 0) )) accentColor = getThemeAttrColor(R.attr.colorPrimary) - inactiveColor = ColorUtils.setAlphaComponent(getThemeAttrColor(R.attr.colorOnSurface), 153) + inactiveColor = getThemeAttrColor(R.attr.colorOnSurface, 153) defaultBackgroundColor = overlayProvider.get().compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(8f)) titleState = ALWAYS_SHOW currentItem = startMenuIndex @@ -167,7 +166,7 @@ class MainActivity : BaseActivity(), MainView { } override fun showActionBarElevation(show: Boolean) { - ViewCompat.setElevation(mainToolbar, if (show) dpToPx(4f) else 0f) + ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) } override fun notifyMenuViewReselected() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt index 1610d029..03f6ac38 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt @@ -14,7 +14,6 @@ import io.github.wulkanowy.ui.modules.about.license.LicenseModule import io.github.wulkanowy.ui.modules.about.logviewer.LogViewerFragment import io.github.wulkanowy.ui.modules.account.AccountDialog import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.attendance.AttendanceModule import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -26,7 +25,6 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.MessageModule import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment -import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceModule import io.github.wulkanowy.ui.modules.mobiledevice.token.MobileDeviceTokenDialog import io.github.wulkanowy.ui.modules.more.MoreFragment import io.github.wulkanowy.ui.modules.note.NoteFragment @@ -52,7 +50,7 @@ abstract class MainModule { } @PerFragment - @ContributesAndroidInjector(modules = [AttendanceModule::class]) + @ContributesAndroidInjector abstract fun bindAttendanceFragment(): AttendanceFragment @PerFragment @@ -116,7 +114,7 @@ abstract class MainModule { abstract fun bindAccountDialog(): AccountDialog @PerFragment - @ContributesAndroidInjector(modules = [MobileDeviceModule::class]) + @ContributesAndroidInjector abstract fun bindMobileDevices(): MobileDeviceFragment @PerFragment diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 7a3e135d..4a9d217b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -1,16 +1,15 @@ package io.github.wulkanowy.ui.modules.message import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.repositories.message.MessageFolder.RECEIVED import io.github.wulkanowy.data.repositories.message.MessageFolder.SENT import io.github.wulkanowy.data.repositories.message.MessageFolder.TRASHED +import io.github.wulkanowy.databinding.FragmentMessageBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter import io.github.wulkanowy.ui.modules.main.MainView @@ -18,10 +17,10 @@ import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity import io.github.wulkanowy.ui.modules.message.tab.MessageTabFragment import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.setOnSelectPageListener -import kotlinx.android.synthetic.main.fragment_message.* import javax.inject.Inject -class MessageFragment : BaseFragment(), MessageView, MainView.TitledView { +class MessageFragment : BaseFragment(R.layout.fragment_message), + MessageView, MainView.TitledView { @Inject lateinit var presenter: MessagePresenter @@ -35,20 +34,17 @@ class MessageFragment : BaseFragment(), MessageView, MainView.TitledView { override val titleStringId get() = R.string.message_title - override val currentPageIndex get() = messageViewPager.currentItem + override val currentPageIndex get() = binding.messageViewPager.currentItem - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_message, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentMessageBinding.bind(view) presenter.onAttachView(this) } override fun initView() { with(pagerAdapter) { - containerId = messageViewPager.id + containerId = binding.messageViewPager.id addFragmentsWithTitle(mapOf( MessageTabFragment.newInstance(RECEIVED) to getString(R.string.message_inbox), MessageTabFragment.newInstance(SENT) to getString(R.string.message_sent), @@ -56,27 +52,29 @@ class MessageFragment : BaseFragment(), MessageView, MainView.TitledView { )) } - with(messageViewPager) { + with(binding.messageViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } - with(messageTabLayout) { - setupWithViewPager(messageViewPager) + with(binding.messageTabLayout) { + setupWithViewPager(binding.messageViewPager) setElevationCompat(context.dpToPx(4f)) } - openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } + binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } } override fun showContent(show: Boolean) { - messageViewPager.visibility = if (show) VISIBLE else INVISIBLE - messageTabLayout.visibility = if (show) VISIBLE else INVISIBLE + with(binding) { + messageViewPager.visibility = if (show) VISIBLE else INVISIBLE + messageTabLayout.visibility = if (show) VISIBLE else INVISIBLE + } } override fun showProgress(show: Boolean) { - messageProgress.visibility = if (show) VISIBLE else INVISIBLE + binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE } fun onDeleteMessage(message: Message) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageItem.kt deleted file mode 100644 index 7e52233c..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageItem.kt +++ /dev/null @@ -1,67 +0,0 @@ -package io.github.wulkanowy.ui.modules.message - -import android.graphics.Typeface.BOLD -import android.graphics.Typeface.NORMAL -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 io.github.wulkanowy.data.db.entities.Message -import io.github.wulkanowy.data.repositories.message.MessageFolder -import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_message.* - -class MessageItem(val message: Message, private val noSubjectString: String) : - AbstractFlexibleItem() { - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun getLayoutRes() = R.layout.item_message - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - holder.apply { - val style = if (message.unread) BOLD else NORMAL - - messageItemAuthor.run { - text = if (message.folderId == MessageFolder.SENT.id) message.recipient else message.sender - setTypeface(null, style) - } - messageItemSubject.run { - text = if (message.subject.isNotBlank()) message.subject else noSubjectString - setTypeface(null, style) - } - messageItemDate.run { - text = message.date.toFormattedString() - setTypeface(null, style) - } - with(messageItemAttachmentIcon) { - visibility = if (message.hasAttachments) View.VISIBLE else View.GONE - } - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MessageItem - - if (message != other.message) return false - return true - } - - override fun hashCode(): Int { - return message.hashCode() - } - - class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : - FlexibleViewHolder(view, adapter), LayoutContainer { - override val containerView: View - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt index a0ac6ec7..436dee53 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt @@ -10,10 +10,11 @@ import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.repositories.message.MessageFolder +import io.github.wulkanowy.databinding.ItemMessageAttachmentBinding +import io.github.wulkanowy.databinding.ItemMessageDividerBinding +import io.github.wulkanowy.databinding.ItemMessagePreviewBinding import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.synthetic.main.item_message_attachment.view.* -import kotlinx.android.synthetic.main.item_message_preview.view.* import javax.inject.Inject class MessagePreviewAdapter @Inject constructor() : @@ -45,44 +46,47 @@ class MessagePreviewAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.MESSAGE.id -> MessageViewHolder(inflater.inflate(R.layout.item_message_preview, parent, false)) - ViewType.DIVIDER.id -> DividerViewHolder(inflater.inflate(R.layout.item_message_divider, parent, false)) - ViewType.ATTACHMENT.id -> AttachmentViewHolder(inflater.inflate(R.layout.item_message_attachment, parent, false)) + ViewType.MESSAGE.id -> MessageViewHolder(ItemMessagePreviewBinding.inflate(inflater, parent, false)) + ViewType.DIVIDER.id -> DividerViewHolder(ItemMessageDividerBinding.inflate(inflater, parent, false)) + ViewType.ATTACHMENT.id -> AttachmentViewHolder(ItemMessageAttachmentBinding.inflate(inflater, parent, false)) else -> throw IllegalStateException() } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is MessageViewHolder -> bindMessage(holder.view, requireNotNull(messageWithAttachment).message) - is AttachmentViewHolder -> bindAttachment(holder.view, requireNotNull(messageWithAttachment).attachments[position - 2]) + is MessageViewHolder -> bindMessage(holder, requireNotNull(messageWithAttachment).message) + is AttachmentViewHolder -> bindAttachment(holder, requireNotNull(messageWithAttachment).attachments[position - 2]) } } @SuppressLint("SetTextI18n") - private fun bindMessage(view: View, message: Message) { - with(view) { - messagePreviewSubject.text = if (message.subject.isNotBlank()) message.subject else context.getString(R.string.message_no_subject) - messagePreviewDate.text = context.getString(R.string.message_date, message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")) + private fun bindMessage(holder: MessageViewHolder, message: Message) { + with(holder.binding) { + messagePreviewSubject.text = if (message.subject.isNotBlank()) message.subject else root.context.getString(R.string.message_no_subject) + messagePreviewDate.text = root.context.getString(R.string.message_date, message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")) messagePreviewContent.text = message.content - messagePreviewAuthor.text = if (message.folderId == MessageFolder.SENT.id) "${context.getString(R.string.message_to)} ${message.recipient}" - else "${context.getString(R.string.message_from)} ${message.sender}" + messagePreviewAuthor.text = if (message.folderId == MessageFolder.SENT.id) "${root.context.getString(R.string.message_to)} ${message.recipient}" + else "${root.context.getString(R.string.message_from)} ${message.sender}" } } - private fun bindAttachment(view: View, attachment: MessageAttachment) { - with(view) { + private fun bindAttachment(holder: AttachmentViewHolder, attachment: MessageAttachment) { + with(holder.binding) { messagePreviewAttachment.visibility = View.VISIBLE messagePreviewAttachment.text = attachment.filename - setOnClickListener { - context.openInternetBrowser(attachment.url) { } + root.setOnClickListener { + root.context.openInternetBrowser(attachment.url) { } } } } - class MessageViewHolder(val view: View) : RecyclerView.ViewHolder(view) + class MessageViewHolder(val binding: ItemMessagePreviewBinding) : + RecyclerView.ViewHolder(binding.root) - class DividerViewHolder(val view: View) : RecyclerView.ViewHolder(view) + class DividerViewHolder(val binding: ItemMessageDividerBinding) : + RecyclerView.ViewHolder(binding.root) - class AttachmentViewHolder(val view: View) : RecyclerView.ViewHolder(view) + class AttachmentViewHolder(val binding: ItemMessageAttachmentBinding) : + RecyclerView.ViewHolder(binding.root) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index 953238c0..99eede15 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -1,27 +1,27 @@ package io.github.wulkanowy.ui.modules.message.preview import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment +import io.github.wulkanowy.databinding.FragmentMessagePreviewBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity -import kotlinx.android.synthetic.main.fragment_message_preview.* import javax.inject.Inject -class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.TitledView { +class MessagePreviewFragment : + BaseFragment(R.layout.fragment_message_preview), + MessagePreviewView, MainView.TitledView { @Inject lateinit var presenter: MessagePreviewPresenter @@ -56,20 +56,17 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl setHasOptionsMenu(true) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_message_preview, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = messagePreviewContainer + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentMessagePreviewBinding.bind(view) + messageContainer = binding.messagePreviewContainer presenter.onAttachView(this, (savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message) } override fun initView() { - messagePreviewErrorDetails.setOnClickListener { presenter.onDetailsClick() } + binding.messagePreviewErrorDetails.setOnClickListener { presenter.onDetailsClick() } - with(messagePreviewRecycler) { + with(binding.messagePreviewRecycler) { layoutManager = LinearLayoutManager(context) adapter = previewAdapter } @@ -100,11 +97,11 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl } override fun showProgress(show: Boolean) { - messagePreviewProgress.visibility = if (show) VISIBLE else GONE + binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - messagePreviewRecycler.visibility = if (show) VISIBLE else GONE + binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE } override fun showOptions(show: Boolean) { @@ -122,15 +119,15 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl } override fun showErrorView(show: Boolean) { - messagePreviewError.visibility = if (show) VISIBLE else GONE + binding.messagePreviewError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - messagePreviewErrorMessage.text = message + binding.messagePreviewErrorMessage.text = message } override fun setErrorRetryCallback(callback: () -> Unit) { - messagePreviewErrorRetry.setOnClickListener { callback() } + binding.messagePreviewErrorRetry.setOnClickListener { callback() } } override fun openMessageReply(message: Message?) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt index 232b05d7..7b750343 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt @@ -14,14 +14,14 @@ import android.widget.Toast.LENGTH_LONG import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.ReportingUnit +import io.github.wulkanowy.databinding.ActivitySendMessageBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.showSoftInput -import kotlinx.android.synthetic.main.activity_send_message.* import javax.inject.Inject -class SendMessageActivity : BaseActivity(), SendMessageView { +class SendMessageActivity : BaseActivity(), SendMessageView { @Inject override lateinit var presenter: SendMessagePresenter @@ -41,17 +41,17 @@ class SendMessageActivity : BaseActivity(), SendMessageVie } override val isDropdownListVisible: Boolean - get() = sendMessageTo.isDropdownListVisible + get() = binding.sendMessageTo.isDropdownListVisible @Suppress("UNCHECKED_CAST") override val formRecipientsData: List - get() = sendMessageTo.addedChipItems as List + get() = binding.sendMessageTo.addedChipItems as List override val formSubjectValue: String - get() = sendMessageSubject.text.toString() + get() = binding.sendMessageSubject.text.toString() override val formContentValue: String - get() = sendMessageMessageContent.text.toString() + get() = binding.sendMessageMessageContent.text.toString() override val messageRequiredRecipients: String get() = getString(R.string.message_required_recipients) @@ -64,18 +64,20 @@ class SendMessageActivity : BaseActivity(), SendMessageVie override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_send_message) - setSupportActionBar(sendMessageToolbar) + setContentView(ActivitySendMessageBinding.inflate(layoutInflater).apply { binding = this }.root) + setSupportActionBar(binding.sendMessageToolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) - messageContainer = sendMessageContainer + messageContainer = binding.sendMessageContainer presenter.onAttachView(this, intent.getSerializableExtra(EXTRA_MESSAGE) as? Message, intent.getSerializableExtra(EXTRA_REPLY) as? Boolean) } override fun initView() { setUpExtendedHitArea() - sendMessageScroll.setOnTouchListener { _, _ -> presenter.onTouchScroll() } - sendMessageTo.onTextChangeListener = presenter::onRecipientsTextChange + with(binding) { + sendMessageScroll.setOnTouchListener { _, _ -> presenter.onTouchScroll() } + sendMessageTo.onTextChangeListener = presenter::onRecipientsTextChange + } } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -93,27 +95,27 @@ class SendMessageActivity : BaseActivity(), SendMessageVie } override fun setReportingUnit(unit: ReportingUnit) { - sendMessageFrom.text = unit.senderName + binding.sendMessageFrom.text = unit.senderName } override fun setRecipients(recipients: List) { - sendMessageTo.filterableChipItems = recipients + binding.sendMessageTo.filterableChipItems = recipients } override fun setSelectedRecipients(recipients: List) { - sendMessageTo.addChips(recipients) + binding.sendMessageTo.addChips(recipients) } override fun showProgress(show: Boolean) { - sendMessageProgress.visibility = if (show) VISIBLE else GONE + binding.sendMessageProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - sendMessageContent.visibility = if (show) VISIBLE else GONE + binding.sendMessageContent.visibility = if (show) VISIBLE else GONE } override fun showEmpty(show: Boolean) { - sendMessageEmpty.visibility = if (show) VISIBLE else GONE + binding.sendMessageEmpty.visibility = if (show) VISIBLE else GONE } override fun showActionBar(show: Boolean) { @@ -121,11 +123,11 @@ class SendMessageActivity : BaseActivity(), SendMessageVie } override fun setSubject(subject: String) { - sendMessageSubject.setText(subject) + binding.sendMessageSubject.setText(subject) } override fun setContent(content: String) { - sendMessageMessageContent.setText(content) + binding.sendMessageMessageContent.setText(content) } override fun showMessage(text: String) { @@ -137,12 +139,14 @@ class SendMessageActivity : BaseActivity(), SendMessageVie } override fun hideDropdownList() { - sendMessageTo.hideDropdownList() + binding.sendMessageTo.hideDropdownList() } override fun scrollToRecipients() { - sendMessageScroll.post { - sendMessageScroll.scrollTo(0, sendMessageTo.bottom - dpToPx(53f).toInt()) + with(binding.sendMessageScroll) { + post { + scrollTo(0, binding.sendMessageTo.bottom - dpToPx(53f).toInt()) + } } } @@ -153,24 +157,24 @@ class SendMessageActivity : BaseActivity(), SendMessageVie private fun setUpExtendedHitArea() { fun extendHitArea() { val containerHitRect = Rect().apply { - sendMessageContent.getHitRect(this) + binding.sendMessageContent.getHitRect(this) } val contentHitRect = Rect().apply { - sendMessageMessageContent.getHitRect(this) + binding.sendMessageMessageContent.getHitRect(this) } contentHitRect.top = contentHitRect.bottom contentHitRect.bottom = containerHitRect.bottom - sendMessageContent.touchDelegate = TouchDelegate(contentHitRect, sendMessageMessageContent) + binding.sendMessageContent.touchDelegate = TouchDelegate(contentHitRect, binding.sendMessageMessageContent) } - sendMessageMessageContent.post { - sendMessageMessageContent.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + with(binding.sendMessageMessageContent) { + post { + addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> extendHitArea() } extendHitArea() } - extendHitArea() } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt new file mode 100644 index 00000000..a889dfef --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt @@ -0,0 +1,85 @@ +package io.github.wulkanowy.ui.modules.message.tab + +import android.graphics.Typeface +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SortedList +import androidx.recyclerview.widget.SortedListAdapterCallback +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Message +import io.github.wulkanowy.data.repositories.message.MessageFolder +import io.github.wulkanowy.databinding.ItemMessageBinding +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class MessageTabAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var onClickListener: (Message, position: Int) -> Unit = { _, _ -> } + + private val items = SortedList(Message::class.java, object : + SortedListAdapterCallback(this) { + + override fun compare(item1: Message, item2: Message): Int { + return item2.date.compareTo(item1.date) + } + + override fun areContentsTheSame(oldItem: Message?, newItem: Message?): Boolean { + return oldItem == newItem + } + + override fun areItemsTheSame(item1: Message, item2: Message): Boolean { + return item1 == item2 + } + }) + + fun replaceAll(models: List) { + items.beginBatchedUpdates() + for (i in items.size() - 1 downTo 0) { + val model = items.get(i) + if (model !in models) { + items.remove(model) + } + } + items.addAll(models) + items.endBatchedUpdates() + } + + fun updateItem(position: Int, item: Message) { + items.updateItemAt(position, item) + } + + override fun getItemCount() = items.size() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val item = items[position] + + with(holder.binding) { + val style = if (item.unread) Typeface.BOLD else Typeface.NORMAL + + messageItemAuthor.run { + text = if (item.folderId == MessageFolder.SENT.id) item.recipient else item.sender + setTypeface(null, style) + } + messageItemSubject.run { + text = if (item.subject.isNotBlank()) item.subject else context.getString(R.string.message_no_subject) + setTypeface(null, style) + } + messageItemDate.run { + text = item.date.toFormattedString() + setTypeface(null, style) + } + messageItemAttachmentIcon.visibility = if (item.hasAttachments) View.VISIBLE else View.GONE + + root.setOnClickListener { onClickListener(item, position) } + } + } + + class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index ce0fc780..909bb687 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt @@ -1,35 +1,33 @@ package io.github.wulkanowy.ui.modules.message.tab import android.os.Bundle -import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.FlexibleItemDecoration -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.appcompat.widget.SearchView +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.repositories.message.MessageFolder +import io.github.wulkanowy.databinding.FragmentMessageTabBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.message.MessageItem import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_message_tab.* +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import javax.inject.Inject -class MessageTabFragment : BaseFragment(), MessageTabView { +class MessageTabFragment : BaseFragment(R.layout.fragment_message_tab), + MessageTabView { @Inject lateinit var presenter: MessageTabPresenter @Inject - lateinit var tabAdapter: FlexibleAdapter> + lateinit var tabAdapter: MessageTabAdapter companion object { const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id" @@ -43,78 +41,92 @@ class MessageTabFragment : BaseFragment(), MessageTabView { } } - override val noSubjectString: String - get() = getString(R.string.message_no_subject) - override val isViewEmpty - get() = tabAdapter.isEmpty + get() = tabAdapter.itemCount == 0 - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_message_tab, container, false) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = messageTabRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentMessageTabBinding.bind(view) + messageContainer = binding.messageTabRecycler presenter.onAttachView(this, MessageFolder.valueOf( (savedInstanceState ?: arguments)?.getString(MESSAGE_TAB_FOLDER_ID).orEmpty() )) } override fun initView() { - tabAdapter.setOnItemClickListener { presenter.onMessageItemSelected(it) } + tabAdapter.onClickListener = presenter::onMessageItemSelected - messageTabRecycler.run { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.messageTabRecycler) { + layoutManager = LinearLayoutManager(context) adapter = tabAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider() - .withDrawDividerOnLastItem(false) - ) + addItemDecoration(DividerItemDecoration(context)) + } + with(binding) { + messageTabSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + messageTabErrorRetry.setOnClickListener { presenter.onRetry() } + messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - messageTabSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - messageTabErrorRetry.setOnClickListener { presenter.onRetry() } - messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - override fun updateData(data: List) { - tabAdapter.updateDataSet(data) + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.action_menu_message_tab, menu) + + val searchView = menu.findItem(R.id.action_search).actionView as SearchView + searchView.queryHint = getString(R.string.all_search_hint) + searchView.maxWidth = Int.MAX_VALUE + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String) = false + override fun onQueryTextChange(query: String): Boolean { + presenter.onSearchQueryTextChange(query) + return true + } + }) } - override fun updateItem(item: AbstractFlexibleItem<*>) { - tabAdapter.updateItem(item) + override fun updateData(data: List) { + tabAdapter.replaceAll(data) } - override fun clearView() { - tabAdapter.clear() + override fun updateItem(item: Message, position: Int) { + tabAdapter.updateItem(position, item) } override fun showProgress(show: Boolean) { - messageTabProgress.visibility = if (show) VISIBLE else GONE + binding.messageTabProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - messageTabSwipe.isEnabled = enable + binding.messageTabSwipe.isEnabled = enable + } + + override fun resetListPosition() { + binding.messageTabRecycler.scrollToPosition(0) } override fun showContent(show: Boolean) { - messageTabRecycler.visibility = if (show) VISIBLE else INVISIBLE + binding.messageTabRecycler.visibility = if (show) VISIBLE else INVISIBLE } override fun showEmpty(show: Boolean) { - messageTabEmpty.visibility = if (show) VISIBLE else INVISIBLE + binding.messageTabEmpty.visibility = if (show) VISIBLE else INVISIBLE } override fun showErrorView(show: Boolean) { - messageTabError.visibility = if (show) VISIBLE else GONE + binding.messageTabError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - messageTabErrorMessage.text = message + binding.messageTabErrorMessage.text = message } override fun showRefresh(show: Boolean) { - messageTabSwipe.isRefreshing = show + binding.messageTabSwipe.isRefreshing = show } override fun openMessage(message: Message) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index cbdb89b2..f96fb6c2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -1,15 +1,16 @@ package io.github.wulkanowy.ui.modules.message.tab -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import android.annotation.SuppressLint +import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.repositories.message.MessageFolder import io.github.wulkanowy.data.repositories.message.MessageRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.ui.modules.message.MessageItem import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.SchedulersProvider +import io.github.wulkanowy.utils.toFormattedString import timber.log.Timber import javax.inject.Inject @@ -26,6 +27,10 @@ class MessageTabPresenter @Inject constructor( private lateinit var lastError: Throwable + private var lastSearchQuery = "" + + private var messages = emptyList() + fun onAttachView(view: MessageTabView, folder: MessageFolder) { super.onAttachView(view) view.initView() @@ -58,15 +63,13 @@ class MessageTabPresenter @Inject constructor( loadData(forceRefresh) } - fun onMessageItemSelected(item: AbstractFlexibleItem<*>) { - if (item is MessageItem) { - Timber.i("Select message ${item.message.id} item") - view?.run { - openMessage(item.message) - if (item.message.unread) { - item.message.unread = false - updateItem(item) - } + fun onMessageItemSelected(message: Message, position: Int) { + Timber.i("Select message ${message.id} item") + view?.run { + openMessage(message) + if (message.unread) { + message.unread = false + updateItem(message, position) } } } @@ -79,7 +82,6 @@ class MessageTabPresenter @Inject constructor( .flatMap { student -> semesterRepository.getCurrentSemester(student) .flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) } - .map { items -> items.map { MessageItem(it, view?.noSubjectString.orEmpty()) } } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) @@ -93,12 +95,8 @@ class MessageTabPresenter @Inject constructor( } .subscribe({ Timber.i("Loading $folder message result: Success") - view?.run { - showEmpty(it.isEmpty()) - showContent(it.isNotEmpty()) - showErrorView(false) - updateData(it) - } + messages = it + onSearchQueryTextChange(lastSearchQuery) analytics.logEvent("load_messages", "items" to it.size, "folder" to folder.name) }) { Timber.i("Loading $folder message result: An exception occurred") @@ -117,4 +115,33 @@ class MessageTabPresenter @Inject constructor( } else showError(message, error) } } + + @SuppressLint("DefaultLocale") + fun onSearchQueryTextChange(query: String) { + lastSearchQuery = query + + val lowerCaseQuery = query.toLowerCase() + val filteredList = mutableListOf() + messages.forEach { + if (lowerCaseQuery in it.subject.toLowerCase() || + lowerCaseQuery in it.sender.toLowerCase() || + lowerCaseQuery in it.recipient.toLowerCase() || + lowerCaseQuery in it.date.toFormattedString() + ) { + filteredList.add(it) + } + } + + updateData(filteredList) + } + + private fun updateData(data: List) { + view?.run { + showEmpty(data.isEmpty()) + showContent(data.isNotEmpty()) + showErrorView(false) + updateData(data) + resetListPosition() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt index 92115ed3..f521191c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt @@ -1,23 +1,19 @@ package io.github.wulkanowy.ui.modules.message.tab -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.ui.base.BaseView -import io.github.wulkanowy.ui.modules.message.MessageItem interface MessageTabView : BaseView { - val noSubjectString: String - val isViewEmpty: Boolean fun initView() - fun updateData(data: List) + fun resetListPosition() - fun updateItem(item: AbstractFlexibleItem<*>) + fun updateData(data: List) - fun clearView() + fun updateItem(item: Message, position: Int) fun showProgress(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceAdapter.kt index 27c72ce6..4bc3097d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceAdapter.kt @@ -1,10 +1,38 @@ package io.github.wulkanowy.ui.modules.mobiledevice -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.data.db.entities.MobileDevice +import io.github.wulkanowy.databinding.ItemMobileDeviceBinding +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject -class MobileDeviceAdapter> : FlexibleAdapter(null, null, true) { +class MobileDeviceAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = mutableListOf() var onDeviceUnregisterListener: (device: MobileDevice, position: Int) -> Unit = { _, _ -> } + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemMobileDeviceBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val device = items[position] + + with(holder.binding) { + mobileDeviceItemDate.text = device.date.toFormattedString("dd.MM.yyyy HH:mm:ss") + mobileDeviceItemName.text = device.name + mobileDeviceItemUnregister.setOnClickListener { + onDeviceUnregisterListener(device, position) + } + } + } + + class ItemViewHolder(val binding: ItemMobileDeviceBinding) : + RecyclerView.ViewHolder(binding.root) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt index d47574f6..6a3e5a44 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt @@ -1,31 +1,31 @@ package io.github.wulkanowy.ui.modules.mobiledevice import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.common.FlexibleItemDecoration -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.helpers.UndoHelper -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.core.view.postDelayed +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.MobileDevice +import io.github.wulkanowy.databinding.FragmentMobileDeviceBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.mobiledevice.token.MobileDeviceTokenDialog -import kotlinx.android.synthetic.main.fragment_mobile_device.* +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import javax.inject.Inject -class MobileDeviceFragment : BaseFragment(), MobileDeviceView, MainView.TitledView { +class MobileDeviceFragment : + BaseFragment(R.layout.fragment_mobile_device), MobileDeviceView, + MainView.TitledView { @Inject lateinit var presenter: MobileDevicePresenter @Inject - lateinit var devicesAdapter: MobileDeviceAdapter> + lateinit var devicesAdapter: MobileDeviceAdapter companion object { fun newInstance() = MobileDeviceFragment() @@ -35,92 +35,95 @@ class MobileDeviceFragment : BaseFragment(), MobileDeviceView, MainView.TitledVi get() = R.string.mobile_devices_title override val isViewEmpty: Boolean - get() = devicesAdapter.isEmpty + get() = devicesAdapter.items.isEmpty() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_mobile_device, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = mobileDevicesRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentMobileDeviceBinding.bind(view) + messageContainer = binding.mobileDevicesRecycler presenter.onAttachView(this) } override fun initView() { - with(mobileDevicesRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + devicesAdapter.onDeviceUnregisterListener = presenter::onUnregisterDevice + + with(binding.mobileDevicesRecycler) { + layoutManager = LinearLayoutManager(context) adapter = devicesAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider() - .withDrawDividerOnLastItem(false) - ) + addItemDecoration(DividerItemDecoration(context)) } + + with(binding) { + mobileDevicesSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + mobileDevicesErrorRetry.setOnClickListener { presenter.onRetry() } + mobileDevicesErrorDetails.setOnClickListener { presenter.onDetailsClick() } + mobileDeviceAddButton.setOnClickListener { presenter.onRegisterDevice() } + } + } + + override fun updateData(data: List) { with(devicesAdapter) { - isPermanentDelete = false - onDeviceUnregisterListener = presenter::onUnregisterDevice + items = data.toMutableList() + notifyDataSetChanged() } - mobileDevicesSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - mobileDevicesErrorRetry.setOnClickListener { presenter.onRetry() } - mobileDevicesErrorDetails.setOnClickListener { presenter.onDetailsClick() } - mobileDeviceAddButton.setOnClickListener { presenter.onRegisterDevice() } } - override fun updateData(data: List) { - devicesAdapter.updateDataSet(data) - } - - override fun restoreDeleteItem() { - devicesAdapter.restoreDeletedItems() - } - - override fun clearData() { - devicesAdapter.clear() - } - - override fun showUndo(position: Int, device: MobileDevice) { - val onActionListener = object : UndoHelper.OnActionListener { - override fun onActionConfirmed(action: Int, event: Int) { - presenter.onUnregisterConfirmed(device) - } - - override fun onActionCanceled(action: Int, positions: MutableList?) { - presenter.onUnregisterCancelled() - } + override fun deleteItem(device: MobileDevice, position: Int) { + with(devicesAdapter) { + items.removeAt(position) + notifyItemRemoved(position) + notifyItemRangeChanged(position, itemCount) } + } - UndoHelper(devicesAdapter, onActionListener) - .withConsecutive(false) - .withAction(UndoHelper.Action.REMOVE) - .start(listOf(position), mobileDevicesRecycler, R.string.mobile_device_removed, R.string.all_undo, 3000) + override fun restoreDeleteItem(device: MobileDevice, position: Int) { + with(devicesAdapter) { + items.add(position, device) + notifyItemInserted(position) + notifyItemRangeChanged(position, itemCount) + } + } + + override fun showUndo(device: MobileDevice, position: Int) { + var confirmed = true + + Snackbar.make(binding.mobileDevicesRecycler, getString(R.string.mobile_device_removed), 3000) + .setAction(R.string.all_undo) { + confirmed = false + presenter.onUnregisterCancelled(device, position) + }.show() + + view?.postDelayed(3000) { + if (confirmed) presenter.onUnregisterConfirmed(device) + } } override fun hideRefresh() { - mobileDevicesSwipe.isRefreshing = false + binding.mobileDevicesSwipe.isRefreshing = false } override fun showProgress(show: Boolean) { - mobileDevicesProgress.visibility = if (show) VISIBLE else GONE + binding.mobileDevicesProgress.visibility = if (show) VISIBLE else GONE } override fun showEmpty(show: Boolean) { - mobileDevicesEmpty.visibility = if (show) VISIBLE else GONE + binding.mobileDevicesEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - mobileDevicesError.visibility = if (show) VISIBLE else GONE + binding.mobileDevicesError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - mobileDevicesErrorMessage.text = message + binding.mobileDevicesErrorMessage.text = message } override fun enableSwipe(enable: Boolean) { - mobileDevicesSwipe.isEnabled = enable + binding.mobileDevicesSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - mobileDevicesRecycler.visibility = if (show) VISIBLE else GONE + binding.mobileDevicesRecycler.visibility = if (show) VISIBLE else GONE } override fun showTokenDialog() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceItem.kt deleted file mode 100644 index 436c2d0e..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceItem.kt +++ /dev/null @@ -1,53 +0,0 @@ -package io.github.wulkanowy.ui.modules.mobiledevice - -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 io.github.wulkanowy.data.db.entities.MobileDevice -import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_mobile_device.* - -class MobileDeviceItem(val device: MobileDevice) : AbstractFlexibleItem() { - - override fun getLayoutRes() = R.layout.item_mobile_device - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - holder.apply { - mobileDeviceItemDate.text = device.date.toFormattedString("dd.MM.yyyy HH:mm:ss") - mobileDeviceItemName.text = device.name - mobileDeviceItemUnregister.setOnClickListener { - (adapter as MobileDeviceAdapter).onDeviceUnregisterListener(device, holder.flexibleAdapterPosition) - } - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MobileDeviceItem - - if (device.id != other.device.id) return false - return true - } - - override fun hashCode(): Int { - var result = device.hashCode() - result = 31 * result + device.id.toInt() - return result - } - - class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer { - - override val containerView: View - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceModule.kt deleted file mode 100644 index 59bbaa9f..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceModule.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.wulkanowy.ui.modules.mobiledevice - -import dagger.Module -import dagger.Provides -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem - -@Module -class MobileDeviceModule { - - @Provides - fun provideMobileDeviceFlexibleAdapter() = MobileDeviceAdapter>() -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt index a6a83f8a..d8c99b22 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt @@ -54,7 +54,6 @@ class MobileDevicePresenter @Inject constructor( mobileDeviceRepository.getDevices(student, semester, forceRefresh) } } - .map { items -> items.map { MobileDeviceItem(it) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -95,14 +94,15 @@ class MobileDevicePresenter @Inject constructor( fun onUnregisterDevice(device: MobileDevice, position: Int) { view?.run { - showUndo(position, device) + deleteItem(device, position) + showUndo(device, position) showEmpty(isViewEmpty) } } - fun onUnregisterCancelled() { + fun onUnregisterCancelled(device: MobileDevice, position: Int) { view?.run { - restoreDeleteItem() + restoreDeleteItem(device, position) showEmpty(isViewEmpty) } } @@ -116,7 +116,6 @@ class MobileDevicePresenter @Inject constructor( .flatMap { mobileDeviceRepository.getDevices(student, semester, it) } } } - .map { items -> items.map { MobileDeviceItem(it) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt index 869b59bb..ec2d3f87 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt @@ -9,14 +9,16 @@ interface MobileDeviceView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) - fun restoreDeleteItem() + fun deleteItem(device: MobileDevice, position: Int) + + fun restoreDeleteItem(device: MobileDevice, position: Int) + + fun showUndo(device: MobileDevice, position: Int) fun hideRefresh() - fun clearData() - fun showProgress(show: Boolean) fun enableSwipe(enable: Boolean) @@ -29,7 +31,5 @@ interface MobileDeviceView : BaseView { fun setErrorDetails(message: String) - fun showUndo(position: Int, device: MobileDevice) - fun showTokenDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt index 8b81156b..9ac6049e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt @@ -14,11 +14,11 @@ import android.widget.Toast import androidx.core.content.getSystemService import io.github.wulkanowy.R import io.github.wulkanowy.data.pojos.MobileDeviceToken +import io.github.wulkanowy.databinding.DialogMobileDeviceBinding import io.github.wulkanowy.ui.base.BaseDialogFragment -import kotlinx.android.synthetic.main.dialog_mobile_device.* import javax.inject.Inject -class MobileDeviceTokenDialog : BaseDialogFragment(), MobileDeviceTokenVIew { +class MobileDeviceTokenDialog : BaseDialogFragment(), MobileDeviceTokenVIew { @Inject lateinit var presenter: MobileDeviceTokenPresenter @@ -33,7 +33,7 @@ class MobileDeviceTokenDialog : BaseDialogFragment(), MobileDeviceTokenVIew { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_mobile_device, container, false) + return DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -42,24 +42,24 @@ class MobileDeviceTokenDialog : BaseDialogFragment(), MobileDeviceTokenVIew { } override fun initView() { - mobileDeviceDialogClose.setOnClickListener { dismiss() } + binding.mobileDeviceDialogClose.setOnClickListener { dismiss() } } override fun updateData(token: MobileDeviceToken) { - with(mobileDeviceDialogToken) { + with(binding.mobileDeviceDialogToken) { text = token.token setOnClickListener { clickCopy(token.token) } } - with(mobileDeviceDialogSymbol) { + with(binding.mobileDeviceDialogSymbol) { text = token.symbol setOnClickListener { clickCopy(token.symbol) } } - with(mobileDeviceDialogPin) { + with(binding.mobileDeviceDialogPin) { text = token.pin setOnClickListener { clickCopy(token.pin) } } - mobileDeviceQr.setImageBitmap(Base64.decode(token.qr, Base64.DEFAULT).let { + binding.mobileDeviceQr.setImageBitmap(Base64.decode(token.qr, Base64.DEFAULT).let { BitmapFactory.decodeByteArray(it, 0, it.size) }) } @@ -71,11 +71,11 @@ class MobileDeviceTokenDialog : BaseDialogFragment(), MobileDeviceTokenVIew { } override fun hideLoading() { - mobileDeviceDialogProgress.visibility = GONE + binding.mobileDeviceDialogProgress.visibility = GONE } override fun showContent() { - mobileDeviceDialogContent.visibility = VISIBLE + binding.mobileDeviceDialogContent.visibility = VISIBLE } override fun closeDialog() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt new file mode 100644 index 00000000..70587b0c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt @@ -0,0 +1,34 @@ +package io.github.wulkanowy.ui.modules.more + +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.databinding.ItemMoreBinding +import javax.inject.Inject + +class MoreAdapter @Inject constructor() : RecyclerView.Adapter() { + + var items = emptyList>() + + var onClickListener: (name: String) -> Unit = {} + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemMoreBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val (title, drawable) = items[position] + + with(holder.binding) { + moreItemTitle.text = title + moreItemImage.setImageDrawable(drawable) + + root.setOnClickListener { onClickListener(title) } + } + } + + class ItemViewHolder(val binding: ItemMoreBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt index ef9c36fa..a79087de 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt @@ -2,13 +2,10 @@ package io.github.wulkanowy.ui.modules.more import android.graphics.drawable.Drawable import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentMoreBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.about.AboutFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment @@ -21,17 +18,16 @@ import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.utils.getCompatDrawable -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_more.* import javax.inject.Inject -class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.MainChildView { +class MoreFragment : BaseFragment(R.layout.fragment_more), MoreView, + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: MorePresenter @Inject - lateinit var moreAdapter: FlexibleAdapter> + lateinit var moreAdapter: MoreAdapter companion object { fun newInstance() = MoreFragment() @@ -64,20 +60,17 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai override val aboutRes: Pair? get() = context?.run { getString(R.string.about_title) to getCompatDrawable(R.drawable.ic_all_about) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_more, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentMoreBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - moreAdapter.setOnItemClickListener { presenter.onItemSelected(it) } + moreAdapter.onClickListener = presenter::onItemSelected - moreRecycler.apply { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.moreRecycler) { + layoutManager = LinearLayoutManager(context) adapter = moreAdapter } } @@ -86,8 +79,11 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai if (::presenter.isInitialized) presenter.onViewReselected() } - override fun updateData(data: List) { - moreAdapter.updateDataSet(data) + override fun updateData(data: List>) { + with(moreAdapter) { + items = data + notifyDataSetChanged() + } } override fun openMessagesView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt deleted file mode 100644 index 85b604e7..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt +++ /dev/null @@ -1,47 +0,0 @@ -package io.github.wulkanowy.ui.modules.more - -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_more.* - -class MoreItem(val title: String, private val drawable: Drawable?) : AbstractFlexibleItem() { - - override fun getLayoutRes() = R.layout.item_more - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - holder.apply { - moreItemTitle.text = title - moreItemImage.setImageDrawable(drawable) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MoreItem - - if (title != other.title) return false - - return true - } - - override fun hashCode(): Int { - return title.hashCode() - } - - class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer { - override val containerView: View - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt index 096f89e9..593645c1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.more -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -21,11 +20,10 @@ class MorePresenter @Inject constructor( loadData() } - fun onItemSelected(item: AbstractFlexibleItem<*>?) { - if (item !is MoreItem) return - Timber.i("Select more item \"${item.title}\"") + fun onItemSelected(title: String) { + Timber.i("Select more item \"${title}\"") view?.run { - when (item.title) { + when (title) { messagesRes?.first -> openMessagesView() homeworkRes?.first -> openHomeworkView() noteRes?.first -> openNoteView() @@ -47,15 +45,15 @@ class MorePresenter @Inject constructor( Timber.i("Load items for more view") view?.run { updateData(listOfNotNull( - messagesRes?.let { MoreItem(it.first, it.second) }, - homeworkRes?.let { MoreItem(it.first, it.second) }, - noteRes?.let { MoreItem(it.first, it.second) }, - luckyNumberRes?.let { MoreItem(it.first, it.second) }, - mobileDevicesRes?.let { MoreItem(it.first, it.second) }, - schoolAndTeachersRes?.let { MoreItem(it.first, it.second) }, - settingsRes?.let { MoreItem(it.first, it.second) }, - aboutRes?.let { MoreItem(it.first, it.second) }) - ) + messagesRes, + homeworkRes, + noteRes, + luckyNumberRes, + mobileDevicesRes, + schoolAndTeachersRes, + settingsRes, + aboutRes + )) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt index 41008176..922afdfd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt @@ -23,7 +23,7 @@ interface MoreView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List>) fun openSettingsView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteAdapter.kt new file mode 100644 index 00000000..2ffcad94 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteAdapter.kt @@ -0,0 +1,60 @@ +package io.github.wulkanowy.ui.modules.note + +import android.annotation.SuppressLint +import android.graphics.Typeface +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import io.github.wulkanowy.sdk.scrapper.notes.Note.CategoryType +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.databinding.ItemNoteBinding +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class NoteAdapter @Inject constructor() : RecyclerView.Adapter() { + + var items = mutableListOf() + + var onClickListener: (Note, position: Int) -> Unit = { _, _ -> } + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemNoteBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val item = items[position] + + with(holder.binding) { + with(noteItemDate) { + text = item.date.toFormattedString() + setTypeface(null, if (item.isRead) Typeface.NORMAL else Typeface.BOLD) + } + with(noteItemType) { + text = item.category + setTypeface(null, if (item.isRead) Typeface.NORMAL else Typeface.BOLD) + } + with(noteItemPoints) { + text = "${if (item.points > 0) "+" else ""}${item.points}" + visibility = if (item.isPointsShow) View.VISIBLE else View.GONE + setTextColor(when (CategoryType.getByValue(item.categoryType)) { + CategoryType.POSITIVE -> ContextCompat.getColor(context, R.color.note_positive) + CategoryType.NEGATIVE -> ContextCompat.getColor(context, R.color.note_negative) + else -> context.getThemeAttrColor(android.R.attr.textColorPrimary) + }) + } + noteItemTeacher.text = item.teacher + noteItemContent.text = item.content + + root.setOnClickListener { onClickListener(item, position) } + } + } + + class ItemViewHolder(val binding: ItemNoteBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt index 20ffea4e..6d1b181a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt @@ -9,13 +9,16 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.DialogFragment import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.databinding.DialogNoteBinding import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.synthetic.main.dialog_note.* import io.github.wulkanowy.sdk.scrapper.notes.Note.CategoryType +import io.github.wulkanowy.utils.lifecycleAwareVariable class NoteDialog : DialogFragment() { + private var binding: DialogNoteBinding by lifecycleAwareVariable() + private lateinit var note: Note companion object { @@ -37,19 +40,22 @@ class NoteDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_note, container, false) + return DialogNoteBinding.inflate(inflater).apply { binding = this }.root } @SuppressLint("SetTextI18n") override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - noteDialogDate.text = note.date.toFormattedString() - noteDialogCategory.text = note.category - noteDialogTeacher.text = note.teacher - noteDialogContent.text = note.content + with(binding) { + noteDialogDate.text = note.date.toFormattedString() + noteDialogCategory.text = note.category + noteDialogTeacher.text = note.teacher + noteDialogContent.text = note.content + } + if (note.isPointsShow) { - with(noteDialogPoints) { + with(binding.noteDialogPoints) { text = "${if (note.points > 0) "+" else ""}${note.points}" setTextColor(when (CategoryType.getByValue(note.categoryType)) { CategoryType.POSITIVE -> ContextCompat.getColor(requireContext(), R.color.note_positive) @@ -58,6 +64,7 @@ class NoteDialog : DialogFragment() { }) } } - noteDialogClose.setOnClickListener { dismiss() } + + binding.noteDialogClose.setOnClickListener { dismiss() } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt index e5335e45..47338165 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt @@ -1,31 +1,27 @@ package io.github.wulkanowy.ui.modules.note import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.FlexibleItemDecoration -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.databinding.FragmentNoteBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_note.* +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import javax.inject.Inject -class NoteFragment : BaseFragment(), NoteView, MainView.TitledView { +class NoteFragment : BaseFragment(R.layout.fragment_note), NoteView, + MainView.TitledView { @Inject lateinit var presenter: NotePresenter @Inject - lateinit var noteAdapter: FlexibleAdapter> + lateinit var noteAdapter: NoteAdapter companion object { fun newInstance() = NoteFragment() @@ -35,77 +31,80 @@ class NoteFragment : BaseFragment(), NoteView, MainView.TitledView { get() = R.string.note_title override val isViewEmpty: Boolean - get() = noteAdapter.isEmpty + get() = noteAdapter.items.isEmpty() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_note, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentNoteBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - noteAdapter.run { - setOnItemClickListener { presenter.onNoteItemSelected(it) } - } + noteAdapter.onClickListener = presenter::onNoteItemSelected - noteRecycler.run { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.noteRecycler) { + layoutManager = LinearLayoutManager(context) adapter = noteAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider() - .withDrawDividerOnLastItem(false) - ) + addItemDecoration(DividerItemDecoration(context)) + } + with(binding) { + noteSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + noteErrorRetry.setOnClickListener { presenter.onRetry() } + noteErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - noteSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - noteErrorRetry.setOnClickListener { presenter.onRetry() } - noteErrorDetails.setOnClickListener { presenter.onDetailsClick() } } override fun showNoteDialog(note: Note) { (activity as? MainActivity)?.showDialogFragment(NoteDialog.newInstance(note)) } - override fun updateData(data: List) { - noteAdapter.updateDataSet(data, true) + override fun updateData(data: List) { + with(noteAdapter) { + items = data.toMutableList() + notifyDataSetChanged() + } } - override fun updateItem(item: AbstractFlexibleItem<*>) { - noteAdapter.updateItem(item) + override fun updateItem(item: Note, position: Int) { + with(noteAdapter) { + items[position] = item + notifyItemChanged(position) + } } override fun clearData() { - noteAdapter.clear() + with(noteAdapter) { + items = mutableListOf() + notifyDataSetChanged() + } } override fun showEmpty(show: Boolean) { - noteEmpty.visibility = if (show) VISIBLE else GONE + binding.noteEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - noteError.visibility = if (show) VISIBLE else GONE + binding.noteError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - noteErrorMessage.text = message + binding.noteErrorMessage.text = message } override fun showProgress(show: Boolean) { - noteProgress.visibility = if (show) VISIBLE else GONE + binding.noteProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - noteSwipe.isEnabled = enable + binding.noteSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - noteRecycler.visibility = if (show) VISIBLE else GONE + binding.noteRecycler.visibility = if (show) VISIBLE else GONE } override fun hideRefresh() { - noteSwipe.isRefreshing = false + binding.noteSwipe.isRefreshing = false } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteItem.kt deleted file mode 100644 index 53fe6fa9..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteItem.kt +++ /dev/null @@ -1,77 +0,0 @@ -package io.github.wulkanowy.ui.modules.note - -import android.annotation.SuppressLint -import android.graphics.Typeface.BOLD -import android.graphics.Typeface.NORMAL -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import androidx.core.content.ContextCompat -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.Note -import io.github.wulkanowy.sdk.scrapper.notes.Note.CategoryType -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_note.* - -class NoteItem(val note: Note) : AbstractFlexibleItem() { - - override fun getLayoutRes() = R.layout.item_note - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - holder.apply { - with(noteItemDate) { - text = note.date.toFormattedString() - setTypeface(null, if (note.isRead) NORMAL else BOLD) - } - with(noteItemType) { - text = note.category - setTypeface(null, if (note.isRead) NORMAL else BOLD) - } - with(noteItemPoints) { - text = "${if (note.points > 0) "+" else ""}${note.points}" - visibility = if (note.isPointsShow) VISIBLE else GONE - setTextColor(when(CategoryType.getByValue(note.categoryType)) { - CategoryType.POSITIVE -> ContextCompat.getColor(context, R.color.note_positive) - CategoryType.NEGATIVE -> ContextCompat.getColor(context, R.color.note_negative) - else -> context.getThemeAttrColor(android.R.attr.textColorPrimary) - }) - } - noteItemTeacher.text = note.teacher - noteItemContent.text = note.content - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as NoteItem - - if (note != other.note) return false - if (note.id != other.note.id) return false - return true - } - - override fun hashCode(): Int { - var result = note.hashCode() - result = 31 * result + note.id.toInt() - return result - } - - class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : - FlexibleViewHolder(view, adapter), LayoutContainer { - override val containerView: View - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt index 7acf37a4..00df71b9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.note -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.repositories.note.NoteRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository @@ -53,8 +52,7 @@ class NotePresenter @Inject constructor( disposable.add(studentRepository.getCurrentStudent() .flatMap { semesterRepository.getCurrentSemester(it).map { semester -> semester to it } } .flatMap { noteRepository.getNotes(it.second, it.first, forceRefresh) } - .map { items -> items.map { NoteItem(it) } } - .map { items -> items.sortedByDescending { it.note.date } } + .map { items -> items.sortedByDescending { it.date } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -90,16 +88,14 @@ class NotePresenter @Inject constructor( } } - fun onNoteItemSelected(item: AbstractFlexibleItem<*>?) { - if (item is NoteItem) { - Timber.i("Select note item ${item.note.id}") - view?.run { - showNoteDialog(item.note) - if (!item.note.isRead) { - item.note.isRead = true - updateItem(item) - updateNote(item.note) - } + fun onNoteItemSelected(note: Note, position: Int) { + Timber.i("Select note item ${note.id}") + view?.run { + showNoteDialog(note) + if (!note.isRead) { + note.isRead = true + updateItem(note, position) + updateNote(note) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt index a9c9f4f2..a7cbab8f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.note -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.ui.base.BaseView @@ -10,9 +9,9 @@ interface NoteView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) - fun updateItem(item: AbstractFlexibleItem<*>) + fun updateItem(item: Note, position: Int) fun clearData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt index 5f9c0b9a..0245aa07 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt @@ -1,12 +1,11 @@ package io.github.wulkanowy.ui.modules.schoolandteachers import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentSchoolandteachersBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter import io.github.wulkanowy.ui.modules.main.MainView @@ -14,10 +13,11 @@ import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment import io.github.wulkanowy.ui.modules.schoolandteachers.teacher.TeacherFragment import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.setOnSelectPageListener -import kotlinx.android.synthetic.main.fragment_schoolandteachers.* import javax.inject.Inject -class SchoolAndTeachersFragment : BaseFragment(), SchoolAndTeachersView, MainView.TitledView { +class SchoolAndTeachersFragment : + BaseFragment(R.layout.fragment_schoolandteachers), + SchoolAndTeachersView, MainView.TitledView { @Inject lateinit var presenter: SchoolAndTeachersPresenter @@ -31,45 +31,44 @@ class SchoolAndTeachersFragment : BaseFragment(), SchoolAndTeachersView, MainVie override val titleStringId: Int get() = R.string.schoolandteachers_title - override val currentPageIndex get() = schoolandteachersViewPager.currentItem + override val currentPageIndex get() = binding.schoolandteachersViewPager.currentItem - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_schoolandteachers, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentSchoolandteachersBinding.bind(view) presenter.onAttachView(this) } override fun initView() { with(pagerAdapter) { - containerId = schoolandteachersViewPager.id + containerId = binding.schoolandteachersViewPager.id addFragmentsWithTitle(mapOf( SchoolFragment.newInstance() to getString(R.string.school_title), TeacherFragment.newInstance() to getString(R.string.teachers_title) )) } - with(schoolandteachersViewPager) { + with(binding.schoolandteachersViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } - with(schoolandteachersTabLayout) { - setupWithViewPager(schoolandteachersViewPager) + with(binding.schoolandteachersTabLayout) { + setupWithViewPager(binding.schoolandteachersViewPager) setElevationCompat(context.dpToPx(4f)) } } override fun showContent(show: Boolean) { - schoolandteachersViewPager.visibility = if (show) VISIBLE else INVISIBLE - schoolandteachersTabLayout.visibility = if (show) VISIBLE else INVISIBLE + with(binding) { + schoolandteachersViewPager.visibility = if (show) VISIBLE else INVISIBLE + schoolandteachersTabLayout.visibility = if (show) VISIBLE else INVISIBLE + } } override fun showProgress(show: Boolean) { - schoolandteachersProgress.visibility = if (show) VISIBLE else INVISIBLE + binding.schoolandteachersProgress.visibility = if (show) VISIBLE else INVISIBLE } fun onChildFragmentLoaded() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolFragment.kt index 5a7c4cad..722999f8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolFragment.kt @@ -1,89 +1,89 @@ package io.github.wulkanowy.ui.modules.schoolandteachers.school import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.School +import io.github.wulkanowy.databinding.FragmentSchoolBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersChildView import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment import io.github.wulkanowy.utils.openDialer import io.github.wulkanowy.utils.openNavigation -import kotlinx.android.synthetic.main.fragment_school.* import javax.inject.Inject -class SchoolFragment : BaseFragment(), SchoolView, MainView.TitledView, SchoolAndTeachersChildView { +class SchoolFragment : BaseFragment(R.layout.fragment_school), SchoolView, + MainView.TitledView, SchoolAndTeachersChildView { @Inject lateinit var presenter: SchoolPresenter override val titleStringId get() = R.string.school_title - override val isViewEmpty get() = schoolName.text.isBlank() + override val isViewEmpty get() = binding.schoolName.text.isBlank() companion object { fun newInstance() = SchoolFragment() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_school, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentSchoolBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - schoolSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - schoolErrorRetry.setOnClickListener { presenter.onRetry() } - schoolErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + schoolSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + schoolErrorRetry.setOnClickListener { presenter.onRetry() } + schoolErrorDetails.setOnClickListener { presenter.onDetailsClick() } - schoolAddressButton.setOnClickListener { presenter.onAddressSelected() } - schoolTelephoneButton.setOnClickListener { presenter.onTelephoneSelected() } + schoolAddressButton.setOnClickListener { presenter.onAddressSelected() } + schoolTelephoneButton.setOnClickListener { presenter.onTelephoneSelected() } + } } override fun updateData(data: School) { - schoolName.text = data.name - schoolAddress.text = data.address.ifBlank { "-" } - schoolAddressButton.visibility = if (data.address.isNotBlank()) VISIBLE else GONE - schoolTelephone.text = data.contact.ifBlank { "-" } - schoolTelephoneButton.visibility = if (data.contact.isNotBlank()) VISIBLE else GONE - schoolHeadmaster.text = data.headmaster - schoolPedagogue.text = data.pedagogue + with(binding) { + schoolName.text = data.name + schoolAddress.text = data.address.ifBlank { "-" } + schoolAddressButton.visibility = if (data.address.isNotBlank()) VISIBLE else GONE + schoolTelephone.text = data.contact.ifBlank { "-" } + schoolTelephoneButton.visibility = if (data.contact.isNotBlank()) VISIBLE else GONE + schoolHeadmaster.text = data.headmaster + schoolPedagogue.text = data.pedagogue + } } override fun showEmpty(show: Boolean) { - schoolEmpty.visibility = if (show) VISIBLE else GONE + binding.schoolEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - schoolError.visibility = if (show) VISIBLE else GONE + binding.schoolError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - schoolErrorMessage.text = message + binding.schoolErrorMessage.text = message } override fun showProgress(show: Boolean) { - schoolProgress.visibility = if (show) VISIBLE else GONE + binding.schoolProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - schoolSwipe.isEnabled = enable + binding.schoolSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - schoolContent.visibility = if (show) VISIBLE else GONE + binding.schoolContent.visibility = if (show) VISIBLE else GONE } override fun hideRefresh() { - schoolSwipe.isRefreshing = false + binding.schoolSwipe.isRefreshing = false } override fun notifyParentDataLoaded() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherAdapter.kt new file mode 100644 index 00000000..8deeae05 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherAdapter.kt @@ -0,0 +1,40 @@ +package io.github.wulkanowy.ui.modules.schoolandteachers.teacher + +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.R +import io.github.wulkanowy.data.db.entities.Teacher +import io.github.wulkanowy.databinding.ItemTeacherBinding +import javax.inject.Inject + +class TeacherAdapter @Inject constructor() : RecyclerView.Adapter() { + + var items = emptyList() + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemTeacherBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val teacher = items[position] + + with(holder.binding) { + teacherItemName.text = teacher.name + teacherItemSubject.text = if (teacher.subject.isNotBlank()) teacher.subject else root.context.getString(R.string.teacher_no_subject) + if (teacher.shortName.isNotBlank()) { + teacherItemShortName.visibility = View.VISIBLE + teacherItemShortName.text = "[${teacher.shortName}]" + } else { + teacherItemShortName.visibility = View.GONE + } + } + } + + class ItemViewHolder(val binding: ItemTeacherBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherFragment.kt index b6bb9c10..aa50339c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherFragment.kt @@ -1,31 +1,28 @@ package io.github.wulkanowy.ui.modules.schoolandteachers.teacher import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.FlexibleItemDecoration -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Teacher +import io.github.wulkanowy.databinding.FragmentTeacherBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersChildView import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment -import kotlinx.android.synthetic.main.fragment_teacher.* +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import javax.inject.Inject -class TeacherFragment : BaseFragment(), TeacherView, MainView.TitledView, - SchoolAndTeachersChildView { +class TeacherFragment : BaseFragment(R.layout.fragment_teacher), + TeacherView, MainView.TitledView, SchoolAndTeachersChildView { @Inject lateinit var presenter: TeacherPresenter @Inject - lateinit var teacherAdapter: FlexibleAdapter> + lateinit var teacherAdapter: TeacherAdapter companion object { fun newInstance() = TeacherFragment() @@ -37,69 +34,60 @@ class TeacherFragment : BaseFragment(), TeacherView, MainView.TitledView, override val noSubjectString get() = getString(R.string.teacher_no_subject) override val isViewEmpty: Boolean - get() = teacherAdapter.isEmpty + get() = teacherAdapter.items.isEmpty() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_teacher, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentTeacherBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - teacherRecycler.run { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.teacherRecycler) { + layoutManager = LinearLayoutManager(context) adapter = teacherAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider() - .withDrawDividerOnLastItem(false) - ) + addItemDecoration(DividerItemDecoration(context)) + } + with(binding) { + teacherSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + teacherErrorRetry.setOnClickListener { presenter.onRetry() } + teacherErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - teacherSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - teacherErrorRetry.setOnClickListener { presenter.onRetry() } - teacherErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - override fun updateData(data: List) { - teacherAdapter.updateDataSet(data, true) - } - - override fun updateItem(item: AbstractFlexibleItem<*>) { - teacherAdapter.updateItem(item) - } - - override fun clearData() { - teacherAdapter.clear() + override fun updateData(data: List) { + with(teacherAdapter) { + items = data + notifyDataSetChanged() + } } override fun showEmpty(show: Boolean) { - teacherEmpty.visibility = if (show) VISIBLE else GONE + binding.teacherEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - teacherError.visibility = if (show) VISIBLE else GONE + binding.teacherError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - teacherErrorMessage.text = message + binding.teacherErrorMessage.text = message } override fun showProgress(show: Boolean) { - teacherProgress.visibility = if (show) VISIBLE else GONE + binding.teacherProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - teacherSwipe.isEnabled = enable + binding.teacherSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - teacherRecycler.visibility = if (show) VISIBLE else GONE + binding.teacherRecycler.visibility = if (show) VISIBLE else GONE } override fun hideRefresh() { - teacherSwipe.isRefreshing = false + binding.teacherSwipe.isRefreshing = false } override fun notifyParentDataLoaded() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherItem.kt deleted file mode 100644 index 36831774..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherItem.kt +++ /dev/null @@ -1,59 +0,0 @@ -package io.github.wulkanowy.ui.modules.schoolandteachers.teacher - -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.Teacher -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_teacher.* - -class TeacherItem(val teacher: Teacher, private val noSubjectText: String) : AbstractFlexibleItem() { - - override fun getLayoutRes() = R.layout.item_teacher - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): TeacherItem.ViewHolder { - return TeacherItem.ViewHolder(view, adapter) - } - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: TeacherItem.ViewHolder, position: Int, payloads: MutableList?) { - holder.apply { - teacherItemName.text = teacher.name - teacherItemSubject.text = if (teacher.subject.isNotBlank()) teacher.subject else noSubjectText - if (teacher.shortName.isNotBlank()) { - teacherItemShortName.visibility = VISIBLE - teacherItemShortName.text = "[${teacher.shortName}]" - } else { - teacherItemShortName.visibility = GONE - } - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TeacherItem - - if (teacher != other.teacher) return false - if (teacher.id != other.teacher.id) return false - return true - } - - override fun hashCode(): Int { - var result = teacher.hashCode() - result = 31 * result + teacher.id.toInt() - return result - } - - class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer { - override val containerView: View - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt index c5b6cfd6..e888308f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt @@ -58,7 +58,6 @@ class TeacherPresenter @Inject constructor( } } .map { it.filter { teacher -> teacher.name.isNotBlank() } } - .map { items -> items.map { TeacherItem(it, view?.noSubjectString.orEmpty()) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherView.kt index b16be8ff..c655bfad 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherView.kt @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.schoolandteachers.teacher -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.data.db.entities.Teacher import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersChildView @@ -12,14 +12,10 @@ interface TeacherView : BaseView, SchoolAndTeachersChildView { fun initView() - fun updateData(data: List) - - fun updateItem(item: AbstractFlexibleItem<*>) + fun updateData(data: List) fun hideRefresh() - fun clearData() - fun showProgress(show: Boolean) fun enableSwipe(enable: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index fe0a9763..0d1d2565 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -6,6 +6,7 @@ import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import com.thelittlefireman.appkillermanager.AppKillerManager import com.yariksoffice.lingver.Lingver import dagger.android.support.AndroidSupportInjection import io.github.wulkanowy.R @@ -50,6 +51,13 @@ class SettingsFragment : PreferenceFragmentCompat(), true } } + findPreference(getString(R.string.pref_key_notifications_fix_issues))?.run { + isVisible = AppKillerManager.isDeviceSupported() && AppKillerManager.isAnyActionAvailable(requireContext()) + setOnPreferenceClickListener { + presenter.onFixSyncIssuesClicked() + true + } + } } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -91,19 +99,19 @@ class SettingsFragment : PreferenceFragmentCompat(), } override fun showError(text: String, error: Throwable) { - (activity as? BaseActivity<*>)?.showError(text, error) + (activity as? BaseActivity<*, *>)?.showError(text, error) } override fun showMessage(text: String) { - (activity as? BaseActivity<*>)?.showMessage(text) + (activity as? BaseActivity<*, *>)?.showMessage(text) } override fun showExpiredDialog() { - (activity as? BaseActivity<*>)?.showExpiredDialog() + (activity as? BaseActivity<*, *>)?.showExpiredDialog() } override fun openClearLoginView() { - (activity as? BaseActivity<*>)?.openClearLoginView() + (activity as? BaseActivity<*, *>)?.openClearLoginView() } override fun showErrorDetailsDialog(error: Throwable) { @@ -119,6 +127,19 @@ class SettingsFragment : PreferenceFragmentCompat(), .show() } + override fun showFixSyncDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.pref_notify_fix_sync_issues) + .setMessage(R.string.pref_notify_fix_sync_issues_message) + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setPositiveButton(R.string.pref_notify_fix_sync_issues_settings_button) { _, _ -> + AppKillerManager.doActionPowerSaving(requireContext()) + AppKillerManager.doActionAutoStart(requireContext()) + AppKillerManager.doActionNotification(requireContext()) + } + .show() + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt index c8545ac0..bccb6f0b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt @@ -4,6 +4,7 @@ import androidx.work.WorkInfo import com.chuckerteam.chucker.api.ChuckerCollector import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository import io.github.wulkanowy.data.repositories.student.StudentRepository +import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -20,6 +21,7 @@ class SettingsPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val preferencesRepository: PreferencesRepository, + private val timetableNotificationHelper: TimetableNotificationSchedulerHelper, private val analytics: FirebaseAnalyticsHelper, private val syncManager: SyncManager, private val chuckerCollector: ChuckerCollector, @@ -36,17 +38,17 @@ class SettingsPresenter @Inject constructor( fun onSharedPreferenceChanged(key: String) { Timber.i("Change settings $key") - with(preferencesRepository) { + preferencesRepository.apply { when (key) { serviceEnableKey -> with(syncManager) { if (isServiceEnabled) startPeriodicSyncWorker() else stopSyncWorker() } servicesIntervalKey, servicesOnlyWifiKey -> syncManager.startPeriodicSyncWorker(true) isDebugNotificationEnableKey -> chuckerCollector.showNotification = isDebugNotificationEnable appThemeKey -> view?.recreateView() + isUpcomingLessonsNotificationsEnableKey -> if (!isUpcomingLessonsNotificationsEnable) timetableNotificationHelper.cancelNotification() appLanguageKey -> view?.run { updateLanguage(if (appLanguage == "system") appInfo.systemLanguage else appLanguage) recreateView() } - else -> Unit } } analytics.logEvent("setting_changed", "name" to key) @@ -56,6 +58,10 @@ class SettingsPresenter @Inject constructor( view?.showForceSyncDialog() } + fun onFixSyncIssuesClicked() { + view?.showFixSyncDialog() + } + fun onForceSyncDialogSubmit() { view?.run { val successString = syncSuccessString diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt index 4a1b0c76..3786ba4b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt @@ -19,4 +19,5 @@ interface SettingsView : BaseView { fun setSyncInProgress(inProgress: Boolean) fun showForceSyncDialog() + fun showFixSyncDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt index 3e0106d1..23a43750 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt @@ -3,12 +3,13 @@ package io.github.wulkanowy.ui.modules.splash import android.os.Bundle import android.widget.Toast import android.widget.Toast.LENGTH_LONG +import androidx.viewbinding.ViewBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.main.MainActivity import javax.inject.Inject -class SplashActivity : BaseActivity(), SplashView { +class SplashActivity : BaseActivity(), SplashView { @Inject override lateinit var presenter: SplashPresenter diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt new file mode 100644 index 00000000..5354442a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -0,0 +1,294 @@ +package io.github.wulkanowy.ui.modules.timetable + +import android.graphics.Paint +import android.view.LayoutInflater +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.databinding.ItemTimetableBinding +import io.github.wulkanowy.databinding.ItemTimetableSmallBinding +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.isJustFinished +import io.github.wulkanowy.utils.isShowTimeUntil +import io.github.wulkanowy.utils.left +import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.until +import org.threeten.bp.LocalDateTime +import timber.log.Timber +import java.util.Timer +import javax.inject.Inject +import kotlin.concurrent.timer + +class TimetableAdapter @Inject constructor() : RecyclerView.Adapter() { + + private enum class ViewType(val id: Int) { + ITEM_NORMAL(1), + ITEM_SMALL(2) + } + + var items = mutableListOf() + set(value) { + field = value + resetTimers() + } + + var onClickListener: (Timetable) -> Unit = {} + + var showWholeClassPlan: String = "no" + + var showTimers: Boolean = false + + private val timers = mutableMapOf() + + private fun resetTimers() { + Timber.d("Timetable timers reset") + with(timers) { + forEach { (_, timer) -> timer.cancel() } + clear() + } + } + + override fun getItemCount() = items.size + + override fun getItemViewType(position: Int) = when { + !items[position].isStudentPlan && showWholeClassPlan == "small" -> ViewType.ITEM_SMALL.id + else -> ViewType.ITEM_NORMAL.id + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + + return when (viewType) { + ViewType.ITEM_NORMAL.id -> ItemViewHolder(ItemTimetableBinding.inflate(inflater, parent, false)) + ViewType.ITEM_SMALL.id -> SmallItemViewHolder(ItemTimetableSmallBinding.inflate(inflater, parent, false)) + else -> throw IllegalStateException() + } + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + resetTimers() + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val lesson = items[position] + + when (holder) { + is ItemViewHolder -> bindNormalView(holder.binding, lesson, position) + is SmallItemViewHolder -> bindSmallView(holder.binding, lesson) + } + } + + private fun bindSmallView(binding: ItemTimetableSmallBinding, lesson: Timetable) { + with(binding) { + timetableSmallItemNumber.text = lesson.number.toString() + timetableSmallItemSubject.text = lesson.subject + timetableSmallItemTimeStart.text = lesson.start.toFormattedString("HH:mm") + timetableSmallItemRoom.text = lesson.room + timetableSmallItemTeacher.text = lesson.teacher + + bindSubjectStyle(timetableSmallItemSubject, lesson) + bindSmallDescription(binding, lesson) + bindSmallColors(binding, lesson) + + root.setOnClickListener { onClickListener(lesson) } + } + } + + private fun bindNormalView(binding: ItemTimetableBinding, lesson: Timetable, position: Int) { + with(binding) { + timetableItemNumber.text = lesson.number.toString() + timetableItemSubject.text = lesson.subject + timetableItemRoom.text = lesson.room + timetableItemTeacher.text = lesson.teacher + timetableItemTimeStart.text = lesson.start.toFormattedString("HH:mm") + timetableItemTimeFinish.text = lesson.end.toFormattedString("HH:mm") + + bindSubjectStyle(timetableItemSubject, lesson) + bindNormalDescription(binding, lesson) + bindNormalColors(binding, lesson) + + if (lesson.isStudentPlan && showTimers) timers[position] = timer(period = 1000) { + root.post { updateTimeLeft(binding, lesson, position) } + } else { + // reset item on set changed + timetableItemTimeUntil.visibility = GONE + timetableItemTimeLeft.visibility = GONE + } + + root.setOnClickListener { onClickListener(lesson) } + } + } + + private fun getPreviousLesson(position: Int): LocalDateTime? { + return items.filter { it.isStudentPlan }.getOrNull(position - 1 - items.filterIndexed { i, item -> i < position && !item.isStudentPlan }.size)?.let { + if (!it.canceled && it.isStudentPlan) it.end + else null + } + } + + private fun updateTimeLeft(binding: ItemTimetableBinding, lesson: Timetable, position: Int) { + with(binding) { + when { + // before lesson + lesson.isShowTimeUntil(getPreviousLesson(position)) -> { + Timber.d("Show time until lesson: $position") + timetableItemTimeLeft.visibility = GONE + with(timetableItemTimeUntil) { + visibility = VISIBLE + text = context.getString(R.string.timetable_time_until, + if (lesson.until.seconds <= 60) { + context.getString(R.string.timetable_seconds, lesson.until.seconds.toString(10)) + } else { + context.getString(R.string.timetable_minutes, lesson.until.toMinutes().toString(10)) + } + ) + } + } + // after lesson start + lesson.left != null -> { + Timber.d("Show time left lesson: $position") + timetableItemTimeUntil.visibility = GONE + with(timetableItemTimeLeft) { + visibility = VISIBLE + text = context.getString( + R.string.timetable_time_left, + if (lesson.left!!.seconds < 60) { + context.getString(R.string.timetable_seconds, lesson.left?.seconds?.toString(10)) + } else { + context.getString(R.string.timetable_minutes, lesson.left?.toMinutes()?.toString(10)) + } + ) + } + } + // right after lesson finish + lesson.isJustFinished -> { + Timber.d("Show just finished lesson: $position") + timetableItemTimeUntil.visibility = GONE + timetableItemTimeLeft.visibility = VISIBLE + timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished) + } + else -> { + timetableItemTimeUntil.visibility = GONE + timetableItemTimeLeft.visibility = GONE + } + } + } + } + + private fun bindSubjectStyle(subjectView: TextView, lesson: Timetable) { + subjectView.paintFlags = if (lesson.canceled) subjectView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + else subjectView.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() + } + + private fun bindSmallDescription(binding: ItemTimetableSmallBinding, lesson: Timetable) { + with(binding) { + if (lesson.info.isNotBlank() && !lesson.changes) { + timetableSmallItemDescription.visibility = VISIBLE + timetableSmallItemDescription.text = lesson.info + + timetableSmallItemRoom.visibility = GONE + timetableSmallItemTeacher.visibility = GONE + + timetableSmallItemDescription.setTextColor(root.context.getThemeAttrColor( + if (lesson.canceled) R.attr.colorPrimary + else R.attr.colorTimetableChange + )) + } else { + timetableSmallItemDescription.visibility = GONE + timetableSmallItemRoom.visibility = VISIBLE + timetableSmallItemTeacher.visibility = VISIBLE + } + } + } + + private fun bindNormalDescription(binding: ItemTimetableBinding, lesson: Timetable) { + with(binding) { + if (lesson.info.isNotBlank() && !lesson.changes) { + timetableItemDescription.visibility = VISIBLE + timetableItemDescription.text = lesson.info + + timetableItemRoom.visibility = GONE + timetableItemTeacher.visibility = GONE + + timetableItemDescription.setTextColor(root.context.getThemeAttrColor( + if (lesson.canceled) R.attr.colorPrimary + else R.attr.colorTimetableChange + )) + } else { + timetableItemDescription.visibility = GONE + timetableItemRoom.visibility = VISIBLE + timetableItemTeacher.visibility = VISIBLE + } + } + } + + private fun bindSmallColors(binding: ItemTimetableSmallBinding, lesson: Timetable) { + with(binding) { + if (lesson.canceled) { + updateNumberAndSubjectCanceledColor(timetableSmallItemNumber, timetableSmallItemSubject) + } else { + updateNumberColor(timetableSmallItemNumber, lesson) + updateSubjectColor(timetableSmallItemSubject, lesson) + updateRoomColor(timetableSmallItemRoom, lesson) + updateTeacherColor(timetableSmallItemTeacher, lesson) + } + } + } + + private fun bindNormalColors(binding: ItemTimetableBinding, lesson: Timetable) { + with(binding) { + if (lesson.canceled) { + updateNumberAndSubjectCanceledColor(timetableItemNumber, timetableItemSubject) + } else { + updateNumberColor(timetableItemNumber, lesson) + updateSubjectColor(timetableItemSubject, lesson) + updateRoomColor(timetableItemRoom, lesson) + updateTeacherColor(timetableItemTeacher, lesson) + } + } + } + + private fun updateNumberAndSubjectCanceledColor(numberView: TextView, subjectView: TextView) { + numberView.setTextColor(numberView.context.getThemeAttrColor(R.attr.colorPrimary)) + subjectView.setTextColor(subjectView.context.getThemeAttrColor(R.attr.colorPrimary)) + } + + private fun updateNumberColor(numberView: TextView, lesson: Timetable) { + numberView.setTextColor(numberView.context.getThemeAttrColor( + if (lesson.changes || lesson.info.isNotBlank()) R.attr.colorTimetableChange + else android.R.attr.textColorPrimary + )) + } + + private fun updateSubjectColor(subjectView: TextView, lesson: Timetable) { + subjectView.setTextColor(subjectView.context.getThemeAttrColor( + if (lesson.subjectOld.isNotBlank() && lesson.subjectOld != lesson.subject) R.attr.colorTimetableChange + else android.R.attr.textColorPrimary + )) + } + + private fun updateRoomColor(roomView: TextView, lesson: Timetable) { + roomView.setTextColor(roomView.context.getThemeAttrColor( + if (lesson.roomOld.isNotBlank() && lesson.roomOld != lesson.room) R.attr.colorTimetableChange + else android.R.attr.textColorSecondary + )) + } + + private fun updateTeacherColor(teacherTextView: TextView, lesson: Timetable) { + teacherTextView.setTextColor(teacherTextView.context.getThemeAttrColor( + if (lesson.teacherOld.isNotBlank() && lesson.teacherOld != lesson.teacher) R.attr.colorTimetableChange + else android.R.attr.textColorSecondary + )) + } + + private class ItemViewHolder(val binding: ItemTimetableBinding) : + RecyclerView.ViewHolder(binding.root) + + private class SmallItemViewHolder(val binding: ItemTimetableSmallBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt index 57ec5998..8efecf07 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt @@ -11,13 +11,16 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.databinding.DialogTimetableBinding import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.synthetic.main.dialog_timetable.* import org.threeten.bp.LocalDateTime class TimetableDialog : DialogFragment() { + private var binding: DialogTimetableBinding by lifecycleAwareVariable() + private lateinit var lesson: Timetable companion object { @@ -39,13 +42,13 @@ class TimetableDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_timetable, container, false) + return DialogTimetableBinding.inflate(inflater).apply { binding = this }.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - lesson.run { + with(lesson) { setInfo(info, teacher, canceled, changes) setSubject(subject, subjectOld) setTeacher(teacher, teacherOld) @@ -54,74 +57,81 @@ class TimetableDialog : DialogFragment() { setTime(start, end) } - timetableDialogClose.setOnClickListener { dismiss() } + binding.timetableDialogClose.setOnClickListener { dismiss() } } private fun setSubject(subject: String, subjectOld: String) { - timetableDialogSubject.text = subject - if (subjectOld.isNotBlank() && subjectOld != subject) { - timetableDialogSubject.run { - paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG - text = subjectOld - } - timetableDialogSubjectNew.run { - visibility = VISIBLE - text = subject + with(binding) { + timetableDialogSubject.text = subject + if (subjectOld.isNotBlank() && subjectOld != subject) { + timetableDialogSubject.run { + paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG + text = subjectOld + } + timetableDialogSubjectNew.run { + visibility = VISIBLE + text = subject + } } } } private fun setInfo(info: String, teacher: String, canceled: Boolean, changes: Boolean) { - when { - info.isNotBlank() -> { - if (canceled) { - timetableDialogChangesTitle.setTextColor(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - timetableDialogChanges.setTextColor(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - } else { - timetableDialogChangesTitle.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange)) - timetableDialogChanges.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange)) - } + with(binding) { + when { + info.isNotBlank() -> { + if (canceled) { + timetableDialogChangesTitle.setTextColor(requireContext().getThemeAttrColor(R.attr.colorPrimary)) + timetableDialogChanges.setTextColor(requireContext().getThemeAttrColor(R.attr.colorPrimary)) + } else { + timetableDialogChangesTitle.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange)) + timetableDialogChanges.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange)) + } - timetableDialogChanges.text = when { - canceled && !changes -> "Lekcja odwołana: $info" - changes && teacher.isNotBlank() -> "Zastępstwo: $teacher" - changes && teacher.isBlank() -> "Zastępstwo, ${info.decapitalize()}" - else -> info.capitalize() + timetableDialogChanges.text = when { + canceled && !changes -> "Lekcja odwołana: $info" + changes && teacher.isNotBlank() -> "Zastępstwo: $teacher" + changes && teacher.isBlank() -> "Zastępstwo, ${info.decapitalize()}" + else -> info.capitalize() + } + } + else -> { + timetableDialogChangesTitle.visibility = GONE + timetableDialogChanges.visibility = GONE } - } else -> { - timetableDialogChangesTitle.visibility = GONE - timetableDialogChanges.visibility = GONE } } } private fun setTeacher(teacher: String, teacherOld: String) { - when { - teacherOld.isNotBlank() && teacherOld != teacher -> { - timetableDialogTeacher.run { - visibility = VISIBLE - paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG - text = teacherOld - } - if (teacher.isNotBlank()) { - timetableDialogTeacherNew.run { + with(binding) { + when { + teacherOld.isNotBlank() && teacherOld != teacher -> { + timetableDialogTeacher.run { visibility = VISIBLE - text = teacher + paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG + text = teacherOld + } + if (teacher.isNotBlank()) { + timetableDialogTeacherNew.run { + visibility = VISIBLE + text = teacher + } } } - } - teacher.isNotBlank() -> timetableDialogTeacher.text = teacher - else -> { - timetableDialogTeacherTitle.visibility = GONE - timetableDialogTeacher.visibility = GONE + teacher.isNotBlank() -> timetableDialogTeacher.text = teacher + else -> { + timetableDialogTeacherTitle.visibility = GONE + timetableDialogTeacher.visibility = GONE + } } } } private fun setGroup(group: String) { - group.let { + with(binding) { when { - it.isNotBlank() -> timetableDialogGroup.text = it + group.isNotBlank() -> timetableDialogGroup.text = group else -> { timetableDialogGroupTitle.visibility = GONE timetableDialogGroup.visibility = GONE @@ -131,30 +141,32 @@ class TimetableDialog : DialogFragment() { } private fun setRoom(room: String, roomOld: String) { - when { - roomOld.isNotBlank() && roomOld != room -> { - timetableDialogRoom.run { - visibility = VISIBLE - paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG - text = roomOld - } - if (room.isNotBlank()) { - timetableDialogRoomNew.run { + with(binding) { + when { + roomOld.isNotBlank() && roomOld != room -> { + timetableDialogRoom.run { visibility = VISIBLE - text = room + paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG + text = roomOld + } + if (room.isNotBlank()) { + timetableDialogRoomNew.run { + visibility = VISIBLE + text = room + } } } - } - room.isNotBlank() -> timetableDialogRoom.text = room - else -> { - timetableDialogRoomTitle.visibility = GONE - timetableDialogRoom.visibility = GONE + room.isNotBlank() -> timetableDialogRoom.text = room + else -> { + timetableDialogRoomTitle.visibility = GONE + timetableDialogRoom.visibility = GONE + } } } } @SuppressLint("SetTextI18n") private fun setTime(start: LocalDateTime, end: LocalDateTime) { - timetableDialogTime.text = "${start.toFormattedString("HH:mm")} - ${end.toFormattedString("HH:mm")}" + binding.timetableDialogTime.text = "${start.toFormattedString("HH:mm")} - ${end.toFormattedString("HH:mm")}" } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index ef6057df..2f01511a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -1,40 +1,35 @@ package io.github.wulkanowy.ui.modules.timetable import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager import com.wdullaer.materialdatetimepicker.date.DatePickerDialog -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.data.db.entities.Timetable +import io.github.wulkanowy.databinding.FragmentTimetableBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.SchooldaysRangeLimiter import io.github.wulkanowy.utils.dpToPx -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_timetable.* import org.threeten.bp.LocalDate import javax.inject.Inject -class TimetableFragment : BaseFragment(), TimetableView, MainView.MainChildView, - MainView.TitledView { +class TimetableFragment : BaseFragment(R.layout.fragment_timetable), + TimetableView, MainView.MainChildView, MainView.TitledView { @Inject lateinit var presenter: TimetablePresenter @Inject - lateinit var timetableAdapter: FlexibleAdapter> + lateinit var timetableAdapter: TimetableAdapter companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" @@ -44,7 +39,7 @@ class TimetableFragment : BaseFragment(), TimetableView, MainView.MainChildView, override val titleStringId get() = R.string.timetable_title - override val isViewEmpty get() = timetableAdapter.isEmpty + override val isViewEmpty get() = timetableAdapter.items.isEmpty() override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize @@ -53,37 +48,33 @@ class TimetableFragment : BaseFragment(), TimetableView, MainView.MainChildView, setHasOptionsMenu(true) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_timetable, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = timetableRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentTimetableBinding.bind(view) + messageContainer = binding.timetableRecycler presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - timetableAdapter.setOnItemClickListener(presenter::onTimetableItemSelected) + timetableAdapter.onClickListener = presenter::onTimetableItemSelected - with(timetableRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.timetableRecycler) { + layoutManager = LinearLayoutManager(context) adapter = timetableAdapter - addItemDecoration(FlexibleItemDecoration(context) - .withDefaultDivider() - .withDrawDividerOnLastItem(false) - ) + addItemDecoration(DividerItemDecoration(context)) } - timetableSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - timetableErrorRetry.setOnClickListener { presenter.onRetry() } - timetableErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + timetableSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + timetableErrorRetry.setOnClickListener { presenter.onRetry() } + timetableErrorDetails.setOnClickListener { presenter.onDetailsClick() } - timetablePreviousButton.setOnClickListener { presenter.onPreviousDay() } - timetableNavDate.setOnClickListener { presenter.onPickDate() } - timetableNextButton.setOnClickListener { presenter.onNextDay() } + timetablePreviousButton.setOnClickListener { presenter.onPreviousDay() } + timetableNavDate.setOnClickListener { presenter.onPickDate() } + timetableNextButton.setOnClickListener { presenter.onNextDay() } - timetableNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + timetableNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -95,24 +86,32 @@ class TimetableFragment : BaseFragment(), TimetableView, MainView.MainChildView, else false } - override fun updateData(data: List) { - timetableAdapter.updateDataSet(data, true) + override fun updateData(data: List, showWholeClassPlanType: String, showTimetableTimers: Boolean) { + with(timetableAdapter) { + items = data.toMutableList() + showTimers = showTimetableTimers + showWholeClassPlan = showWholeClassPlanType + notifyDataSetChanged() + } } override fun clearData() { - timetableAdapter.clear() + with(timetableAdapter) { + items = mutableListOf() + notifyDataSetChanged() + } } override fun updateNavigationDay(date: String) { - timetableNavDate.text = date + binding.timetableNavDate.text = date } override fun hideRefresh() { - timetableSwipe.isRefreshing = false + binding.timetableSwipe.isRefreshing = false } override fun resetView() { - timetableRecycler.smoothScrollToPosition(0) + binding.timetableRecycler.smoothScrollToPosition(0) } override fun onFragmentReselected() { @@ -124,35 +123,35 @@ class TimetableFragment : BaseFragment(), TimetableView, MainView.MainChildView, } override fun showEmpty(show: Boolean) { - timetableEmpty.visibility = if (show) VISIBLE else GONE + binding.timetableEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - timetableError.visibility = if (show) VISIBLE else GONE + binding.timetableError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - timetableErrorMessage.text = message + binding.timetableErrorMessage.text = message } override fun showProgress(show: Boolean) { - timetableProgress.visibility = if (show) VISIBLE else GONE + binding.timetableProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - timetableSwipe.isEnabled = enable + binding.timetableSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - timetableRecycler.visibility = if (show) VISIBLE else GONE + binding.timetableRecycler.visibility = if (show) VISIBLE else GONE } override fun showPreButton(show: Boolean) { - timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE + binding.timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE } override fun showNextButton(show: Boolean) { - timetableNextButton.visibility = if (show) VISIBLE else View.INVISIBLE + binding.timetableNextButton.visibility = if (show) VISIBLE else View.INVISIBLE } override fun showTimetableDialog(lesson: Timetable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt deleted file mode 100644 index 5b35b85d..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt +++ /dev/null @@ -1,197 +0,0 @@ -package io.github.wulkanowy.ui.modules.timetable - -import android.annotation.SuppressLint -import android.graphics.Paint -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import android.widget.TextView -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.Timetable -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.toFormattedString -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_timetable.* -import kotlinx.android.synthetic.main.item_timetable_small.* - -class TimetableItem(val lesson: Timetable, private val showWholeClassPlan: String) : - AbstractFlexibleItem() { - - override fun getLayoutRes() = when { - showWholeClassPlan == "small" && !lesson.isStudentPlan -> R.layout.item_timetable_small - else -> R.layout.item_timetable - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { - return ViewHolder(view, adapter) - } - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList?) { - when (itemViewType) { - R.layout.item_timetable_small -> bindSmallView(holder) - R.layout.item_timetable -> bindNormalView(holder) - } - } - - private fun bindSmallView(holder: ViewHolder) { - with(holder) { - timetableSmallItemNumber.text = lesson.number.toString() - timetableSmallItemSubject.text = lesson.subject - timetableSmallItemTimeStart.text = lesson.start.toFormattedString("HH:mm") - timetableSmallItemRoom.text = lesson.room - timetableSmallItemTeacher.text = lesson.teacher - - updateSubjectStyle(timetableSmallItemSubject) - updateSmallDescription(this) - updateSmallColors(this) - } - } - - private fun bindNormalView(holder: ViewHolder) { - with(holder) { - timetableItemNumber.text = lesson.number.toString() - timetableItemSubject.text = lesson.subject - timetableItemRoom.text = lesson.room - timetableItemTeacher.text = lesson.teacher - timetableItemTimeStart.text = lesson.start.toFormattedString("HH:mm") - timetableItemTimeFinish.text = lesson.end.toFormattedString("HH:mm") - - updateSubjectStyle(timetableItemSubject) - updateNormalDescription(this) - updateNormalColors(this) - } - } - - private fun updateSubjectStyle(subjectView: TextView) { - subjectView.paintFlags = if (lesson.canceled) subjectView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG - else subjectView.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() - } - - private fun updateSmallDescription(holder: ViewHolder) { - with(holder) { - if (lesson.info.isNotBlank() && !lesson.changes) { - timetableSmallItemDescription.visibility = VISIBLE - timetableSmallItemDescription.text = lesson.info - - timetableSmallItemRoom.visibility = GONE - timetableSmallItemTeacher.visibility = GONE - - timetableSmallItemDescription.setTextColor(holder.view.context.getThemeAttrColor( - if (lesson.canceled) R.attr.colorPrimary - else R.attr.colorTimetableChange - )) - } else { - timetableSmallItemDescription.visibility = GONE - timetableSmallItemRoom.visibility = VISIBLE - timetableSmallItemTeacher.visibility = VISIBLE - } - } - } - - private fun updateNormalDescription(holder: ViewHolder) { - with(holder) { - if (lesson.info.isNotBlank() && !lesson.changes) { - timetableItemDescription.visibility = VISIBLE - timetableItemDescription.text = lesson.info - - timetableItemRoom.visibility = GONE - timetableItemTeacher.visibility = GONE - - timetableItemDescription.setTextColor(holder.view.context.getThemeAttrColor( - if (lesson.canceled) R.attr.colorPrimary - else R.attr.colorTimetableChange - )) - } else { - timetableItemDescription.visibility = GONE - timetableItemRoom.visibility = VISIBLE - timetableItemTeacher.visibility = VISIBLE - } - } - } - - private fun updateSmallColors(holder: ViewHolder) { - with(holder) { - if (lesson.canceled) { - updateNumberAndSubjectCanceledColor(timetableSmallItemNumber, timetableSmallItemSubject) - } else { - updateNumberColor(timetableSmallItemNumber) - updateSubjectColor(timetableSmallItemSubject) - updateRoomColor(timetableSmallItemRoom) - updateTeacherColor(timetableSmallItemTeacher) - } - } - } - - private fun updateNormalColors(holder: ViewHolder) { - with(holder) { - if (lesson.canceled) { - updateNumberAndSubjectCanceledColor(timetableItemNumber, timetableItemSubject) - } else { - updateNumberColor(timetableItemNumber) - updateSubjectColor(timetableItemSubject) - updateRoomColor(timetableItemRoom) - updateTeacherColor(timetableItemTeacher) - } - } - } - - private fun updateNumberAndSubjectCanceledColor(numberView: TextView, subjectView: TextView) { - numberView.setTextColor(numberView.context.getThemeAttrColor(R.attr.colorPrimary)) - subjectView.setTextColor(subjectView.context.getThemeAttrColor(R.attr.colorPrimary)) - } - - private fun updateNumberColor(numberView: TextView) { - numberView.setTextColor(numberView.context.getThemeAttrColor( - if (lesson.changes || lesson.info.isNotBlank()) R.attr.colorTimetableChange - else android.R.attr.textColorPrimary - )) - } - - private fun updateSubjectColor(subjectView: TextView) { - subjectView.setTextColor(subjectView.context.getThemeAttrColor( - if (lesson.subjectOld.isNotBlank() && lesson.subjectOld != lesson.subject) R.attr.colorTimetableChange - else android.R.attr.textColorPrimary - )) - } - - private fun updateRoomColor(roomView: TextView) { - roomView.setTextColor(roomView.context.getThemeAttrColor( - if (lesson.roomOld.isNotBlank() && lesson.roomOld != lesson.room) R.attr.colorTimetableChange - else android.R.attr.textColorSecondary - )) - } - - private fun updateTeacherColor(teacherTextView: TextView) { - teacherTextView.setTextColor(teacherTextView.context.getThemeAttrColor( - if (lesson.teacherOld.isNotBlank() && lesson.teacherOld != lesson.teacher) R.attr.colorTimetableChange - else android.R.attr.textColorSecondary - )) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TimetableItem - - if (lesson != other.lesson) return false - return true - } - - override fun hashCode(): Int { - var result = lesson.hashCode() - result = 31 * result + lesson.id.toInt() - return result - } - - class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : - FlexibleViewHolder(view, adapter), LayoutContainer { - override val containerView: View - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index 2e9d0a0b..50c12364 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.timetable import android.annotation.SuppressLint -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository @@ -22,7 +21,6 @@ import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.of import org.threeten.bp.LocalDate.ofEpochDay import timber.log.Timber -import java.util.concurrent.TimeUnit.MILLISECONDS import javax.inject.Inject class TimetablePresenter @Inject constructor( @@ -102,11 +100,9 @@ class TimetablePresenter @Inject constructor( } } - fun onTimetableItemSelected(item: AbstractFlexibleItem<*>?) { - if (item is TimetableItem) { - Timber.i("Select timetable item ${item.lesson.id}") - view?.showTimetableDialog(item.lesson) - } + fun onTimetableItemSelected(lesson: Timetable) { + Timber.i("Select timetable item ${lesson.id}") + view?.showTimetableDialog(lesson) } fun onCompletedLessonsSwitchSelected(): Boolean { @@ -139,9 +135,8 @@ class TimetablePresenter @Inject constructor( timetableRepository.getTimetable(student, semester, currentDate, currentDate, forceRefresh) } } - .delay(200, MILLISECONDS) - .map { createTimetableItems(it) } - .map { items -> items.sortedWith(compareBy({ it.lesson.number }, { !it.lesson.isStudentPlan })) } + .map { items -> items.filter { if (prefRepository.showWholeClassPlan == "no") it.isStudentPlan else true } } + .map { items -> items.sortedWith(compareBy({ it.number }, { !it.isStudentPlan })) } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -154,7 +149,7 @@ class TimetablePresenter @Inject constructor( .subscribe({ Timber.i("Loading timetable result: Success") view?.apply { - updateData(it) + updateData(it, prefRepository.showWholeClassPlan, prefRepository.showTimetableTimers) showEmpty(it.isEmpty()) showErrorView(false) showContent(it.isNotEmpty()) @@ -178,12 +173,6 @@ class TimetablePresenter @Inject constructor( } } - private fun createTimetableItems(items: List): List { - return items - .filter { if (prefRepository.showWholeClassPlan == "no") it.isStudentPlan else true } - .map { TimetableItem(it, prefRepository.showWholeClassPlan) } - } - private fun reloadView() { Timber.i("Reload timetable view with the date ${currentDate.toFormattedString()}") view?.apply { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt index f730a271..1efa320f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt @@ -12,7 +12,7 @@ interface TimetableView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List, showWholeClassPlanType: String, showTimetableTimers: Boolean) fun updateNavigationDay(date: String) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt index 8f7b1ec5..56ea16cf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt @@ -6,12 +6,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment -import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.CompletedLesson -import kotlinx.android.synthetic.main.dialog_lesson_completed.* +import io.github.wulkanowy.databinding.DialogLessonCompletedBinding +import io.github.wulkanowy.utils.lifecycleAwareVariable class CompletedLessonDialog : DialogFragment() { + private var binding: DialogLessonCompletedBinding by lifecycleAwareVariable() + private lateinit var completedLesson: CompletedLesson companion object { @@ -28,46 +30,54 @@ class CompletedLessonDialog : DialogFragment() { super.onCreate(savedInstanceState) setStyle(STYLE_NO_TITLE, 0) arguments?.run { - completedLesson = getSerializable(CompletedLessonDialog.ARGUMENT_KEY) as CompletedLesson + completedLesson = getSerializable(ARGUMENT_KEY) as CompletedLesson } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.dialog_lesson_completed, container, false) + return DialogLessonCompletedBinding.inflate(inflater).apply { binding = this }.root } @SuppressLint("SetTextI18n") override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - completedLessonDialogSubject.text = completedLesson.subject - completedLessonDialogTopic.text = completedLesson.topic - completedLessonDialogTeacher.text = completedLesson.teacher - completedLessonDialogAbsence.text = completedLesson.absence - completedLessonDialogChanges.text = completedLesson.substitution - completedLessonDialogResources.text = completedLesson.resources + with(binding) { + completedLessonDialogSubject.text = completedLesson.subject + completedLessonDialogTopic.text = completedLesson.topic + completedLessonDialogTeacher.text = completedLesson.teacher + completedLessonDialogAbsence.text = completedLesson.absence + completedLessonDialogChanges.text = completedLesson.substitution + completedLessonDialogResources.text = completedLesson.resources + } completedLesson.substitution.let { if (it.isBlank()) { - completedLessonDialogChangesTitle.visibility = View.GONE - completedLessonDialogChanges.visibility = View.GONE - } else completedLessonDialogChanges.text = it + with(binding) { + completedLessonDialogChangesTitle.visibility = View.GONE + completedLessonDialogChanges.visibility = View.GONE + } + } else binding.completedLessonDialogChanges.text = it } completedLesson.absence.let { if (it.isBlank()) { - completedLessonDialogAbsenceTitle.visibility = View.GONE - completedLessonDialogAbsence.visibility = View.GONE - } else completedLessonDialogAbsence.text = it + with(binding) { + completedLessonDialogAbsenceTitle.visibility = View.GONE + completedLessonDialogAbsence.visibility = View.GONE + } + } else binding.completedLessonDialogAbsence.text = it } completedLesson.resources.let { if (it.isBlank()) { - completedLessonDialogResourcesTitle.visibility = View.GONE - completedLessonDialogResources.visibility = View.GONE - } else completedLessonDialogResources.text = it + with(binding) { + completedLessonDialogResourcesTitle.visibility = View.GONE + completedLessonDialogResources.visibility = View.GONE + } + } else binding.completedLessonDialogResources.text = it } - completedLessonDialogClose.setOnClickListener { dismiss() } + binding.completedLessonDialogClose.setOnClickListener { dismiss() } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonItem.kt deleted file mode 100644 index fd6dc8a6..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonItem.kt +++ /dev/null @@ -1,58 +0,0 @@ -package io.github.wulkanowy.ui.modules.timetable.completed - -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.CompletedLesson -import io.github.wulkanowy.utils.getThemeAttrColor -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_completed_lesson.* - -class CompletedLessonItem(val completedLesson: CompletedLesson) : AbstractFlexibleItem() { - - override fun getLayoutRes() = R.layout.item_completed_lesson - - override fun createViewHolder(view: View?, adapter: FlexibleAdapter>?): CompletedLessonItem.ViewHolder { - return CompletedLessonItem.ViewHolder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: CompletedLessonItem.ViewHolder?, position: Int, payloads: MutableList?) { - holder?.apply { - completedLessonItemNumber.text = completedLesson.number.toString() - completedLessonItemNumber.setTextColor(holder.contentView.context.getThemeAttrColor( - if (completedLesson.substitution.isNotEmpty()) R.attr.colorTimetableChange - else android.R.attr.textColorPrimary - )) - completedLessonItemSubject.text = completedLesson.subject - completedLessonItemTopic.text = completedLesson.topic - completedLessonItemAlert.visibility = if (completedLesson.substitution.isNotEmpty()) VISIBLE else GONE - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as CompletedLessonItem - - if (completedLesson != other.completedLesson) return false - - return true - } - - override fun hashCode(): Int { - return completedLesson.hashCode() - } - - class ViewHolder(view: View?, adapter: FlexibleAdapter>?) : FlexibleViewHolder(view, adapter), - LayoutContainer { - - override val containerView: View? - get() = contentView - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsAdapter.kt new file mode 100644 index 00000000..3399a8a2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsAdapter.kt @@ -0,0 +1,45 @@ +package io.github.wulkanowy.ui.modules.timetable.completed + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.CompletedLesson +import io.github.wulkanowy.databinding.ItemCompletedLessonBinding +import io.github.wulkanowy.utils.getThemeAttrColor +import javax.inject.Inject + +class CompletedLessonsAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = emptyList() + + var onClickListener: (CompletedLesson) -> Unit = {} + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemCompletedLessonBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val item = items[position] + + with(holder.binding) { + completedLessonItemNumber.text = item.number.toString() + completedLessonItemNumber.setTextColor(root.context.getThemeAttrColor( + if (item.substitution.isNotEmpty()) R.attr.colorTimetableChange + else android.R.attr.textColorPrimary + )) + completedLessonItemSubject.text = item.subject + completedLessonItemTopic.text = item.topic + completedLessonItemAlert.visibility = if (item.substitution.isNotEmpty()) View.VISIBLE else View.GONE + + root.setOnClickListener { onClickListener(item) } + } + } + + class ItemViewHolder(val binding: ItemCompletedLessonBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt index 60c16b2d..2efd30a3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt @@ -1,36 +1,34 @@ package io.github.wulkanowy.ui.modules.timetable.completed import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager import com.wdullaer.materialdatetimepicker.date.DatePickerDialog -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.data.db.entities.CompletedLesson +import io.github.wulkanowy.databinding.FragmentTimetableCompletedBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.SchooldaysRangeLimiter import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.getCompatDrawable -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.fragment_timetable_completed.* import org.threeten.bp.LocalDate import javax.inject.Inject -class CompletedLessonsFragment : BaseFragment(), CompletedLessonsView, MainView.TitledView { +class CompletedLessonsFragment : + BaseFragment(R.layout.fragment_timetable_completed), + CompletedLessonsView, MainView.TitledView { @Inject lateinit var presenter: CompletedLessonsPresenter @Inject - lateinit var completedLessonsAdapter: FlexibleAdapter> + lateinit var completedLessonsAdapter: CompletedLessonsAdapter companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" @@ -40,88 +38,96 @@ class CompletedLessonsFragment : BaseFragment(), CompletedLessonsView, MainView. override val titleStringId get() = R.string.completed_lessons_title - override val isViewEmpty get() = completedLessonsAdapter.isEmpty + override val isViewEmpty get() = completedLessonsAdapter.items.isEmpty() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_timetable_completed, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - messageContainer = completedLessonsRecycler + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentTimetableCompletedBinding.bind(view) + messageContainer = binding.completedLessonsRecycler presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - completedLessonsAdapter.setOnItemClickListener(presenter::onCompletedLessonsItemSelected) + completedLessonsAdapter.onClickListener = presenter::onCompletedLessonsItemSelected - with(completedLessonsRecycler) { - layoutManager = SmoothScrollLinearLayoutManager(context) + with(binding.completedLessonsRecycler) { + layoutManager = LinearLayoutManager(context) adapter = completedLessonsAdapter + addItemDecoration(DividerItemDecoration(context)) } - completedLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - completedLessonErrorRetry.setOnClickListener { presenter.onRetry() } - completedLessonErrorDetails.setOnClickListener { presenter.onDetailsClick() } + with(binding) { + completedLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + completedLessonErrorRetry.setOnClickListener { presenter.onRetry() } + completedLessonErrorDetails.setOnClickListener { presenter.onDetailsClick() } - completedLessonsPreviousButton.setOnClickListener { presenter.onPreviousDay() } - completedLessonsNavDate.setOnClickListener { presenter.onPickDate() } - completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } + completedLessonsPreviousButton.setOnClickListener { presenter.onPreviousDay() } + completedLessonsNavDate.setOnClickListener { presenter.onPickDate() } + completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } - completedLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + completedLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + } } - override fun updateData(data: List) { - completedLessonsAdapter.updateDataSet(data, true) + override fun updateData(data: List) { + with(completedLessonsAdapter) { + items = data + notifyDataSetChanged() + } } override fun clearData() { - completedLessonsAdapter.clear() + with(completedLessonsAdapter) { + items = emptyList() + notifyDataSetChanged() + } } override fun updateNavigationDay(date: String) { - completedLessonsNavDate.text = date + binding.completedLessonsNavDate.text = date } override fun hideRefresh() { - completedLessonsSwipe.isRefreshing = false + binding.completedLessonsSwipe.isRefreshing = false } override fun showEmpty(show: Boolean) { - completedLessonsEmpty.visibility = if (show) VISIBLE else GONE + binding.completedLessonsEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - completedLessonError.visibility = if (show) VISIBLE else GONE + binding.completedLessonError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - completedLessonErrorMessage.text = message + binding.completedLessonErrorMessage.text = message } override fun showFeatureDisabled() { - completedLessonsInfo.text = getString(R.string.error_feature_disabled) - completedLessonsInfoImage.setImageDrawable(requireContext().getCompatDrawable(R.drawable.ic_all_close_circle)) + with(binding) { + completedLessonsInfo.text = getString(R.string.error_feature_disabled) + completedLessonsInfoImage.setImageDrawable(requireContext().getCompatDrawable(R.drawable.ic_all_close_circle)) + } } override fun showProgress(show: Boolean) { - completedLessonsProgress.visibility = if (show) VISIBLE else GONE + binding.completedLessonsProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - completedLessonsSwipe.isEnabled = enable + binding.completedLessonsSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - completedLessonsRecycler.visibility = if (show) VISIBLE else GONE + binding.completedLessonsRecycler.visibility = if (show) VISIBLE else GONE } override fun showPreButton(show: Boolean) { - completedLessonsPreviousButton.visibility = if (show) VISIBLE else INVISIBLE + binding.completedLessonsPreviousButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showNextButton(show: Boolean) { - completedLessonsNextButton.visibility = if (show) VISIBLE else INVISIBLE + binding.completedLessonsNextButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showCompletedLessonDialog(completedLesson: CompletedLesson) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt index 001fed97..355411eb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt @@ -1,7 +1,7 @@ package io.github.wulkanowy.ui.modules.timetable.completed import android.annotation.SuppressLint -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.repositories.completedlessons.CompletedLessonsRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository import io.github.wulkanowy.data.repositories.student.StudentRepository @@ -18,7 +18,6 @@ import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.ofEpochDay import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject class CompletedLessonsPresenter @Inject constructor( @@ -87,11 +86,9 @@ class CompletedLessonsPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - fun onCompletedLessonsItemSelected(item: AbstractFlexibleItem<*>?) { - if (item is CompletedLessonItem) { - Timber.i("Select completed lessons item ${item.completedLesson.id}") - view?.showCompletedLessonDialog(item.completedLesson) - } + fun onCompletedLessonsItemSelected(completedLesson: CompletedLesson) { + Timber.i("Select completed lessons item ${completedLesson.id}") + view?.showCompletedLessonDialog(completedLesson) } private fun setBaseDateOnHolidays() { @@ -119,9 +116,7 @@ class CompletedLessonsPresenter @Inject constructor( completedLessonsRepository.getCompletedLessons(student, semester, currentDate, currentDate, forceRefresh) } } - .delay(200, TimeUnit.MILLISECONDS) - .map { items -> items.map { CompletedLessonItem(it) } } - .map { items -> items.sortedBy { it.completedLesson.number } } + .map { items -> items.sortedBy { it.number } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt index a6a327e2..170e1969 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt @@ -10,7 +10,7 @@ interface CompletedLessonsView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) fun clearData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt index 7636637f..3dc7a3a8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt @@ -4,36 +4,41 @@ import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS import android.content.Intent +import android.os.Build import android.os.Bundle import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import androidx.recyclerview.widget.LinearLayoutManager import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.databinding.ActivityWidgetConfigureBinding import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.base.WidgetConfigureAdapter import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.EXTRA_FROM_PROVIDER -import io.github.wulkanowy.utils.setOnItemClickListener -import kotlinx.android.synthetic.main.activity_widget_configure.* +import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject -class TimetableWidgetConfigureActivity : BaseActivity(), +class TimetableWidgetConfigureActivity : + BaseActivity(), TimetableWidgetConfigureView { @Inject - lateinit var configureAdapter: FlexibleAdapter> + lateinit var configureAdapter: WidgetConfigureAdapter @Inject override lateinit var presenter: TimetableWidgetConfigurePresenter + @Inject + lateinit var appInfo: AppInfo + private var dialog: AlertDialog? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setResult(RESULT_CANCELED) - setContentView(R.layout.activity_widget_configure) + setContentView(ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root) intent.extras.let { presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID), it?.getBoolean(EXTRA_FROM_PROVIDER)) @@ -41,19 +46,20 @@ class TimetableWidgetConfigureActivity : BaseActivity= Build.VERSION_CODES.Q) items += getString(R.string.widget_timetable_theme_system) dialog = AlertDialog.Builder(this, R.style.WulkanowyTheme_WidgetAccountSwitcher) .setTitle(R.string.widget_timetable_theme_title) @@ -64,8 +70,11 @@ class TimetableWidgetConfigureActivity : BaseActivity) { - configureAdapter.updateDataSet(data) + override fun updateData(data: List>) { + with(configureAdapter) { + items = data + notifyDataSetChanged() + } } override fun updateTimetableWidget(widgetId: Int) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureItem.kt deleted file mode 100644 index a3338c68..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureItem.kt +++ /dev/null @@ -1,59 +0,0 @@ -package io.github.wulkanowy.ui.modules.timetablewidget - -import android.annotation.SuppressLint -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 TimetableWidgetConfigureItem(val student: Student, private val isCurrent: Boolean) : - AbstractFlexibleItem() { - - override fun getLayoutRes() = R.layout.item_account - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>) = ViewHolder(view, adapter) - - @SuppressLint("SetTextI18n") - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList) { - val context = holder.contentView.context - - val colorImage = if (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) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TimetableWidgetConfigureItem - - 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 - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt index e1685108..57dde824 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.timetablewidget -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.student.StudentRepository @@ -32,13 +31,11 @@ class TimetableWidgetConfigurePresenter @Inject constructor( loadData() } - fun onItemSelect(item: AbstractFlexibleItem<*>) { - if (item is TimetableWidgetConfigureItem) { - selectedStudent = item.student + fun onItemSelect(student: Student) { + selectedStudent = student - if (isFromProvider) registerStudent(selectedStudent) - else view?.showThemeDialog() - } + if (isFromProvider) registerStudent(selectedStudent) + else view?.showThemeDialog() } fun onThemeSelect(index: Int) { @@ -48,7 +45,7 @@ class TimetableWidgetConfigurePresenter @Inject constructor( registerStudent(selectedStudent) } - fun onDismissThemeView(){ + fun onDismissThemeView() { view?.finishView() } @@ -56,7 +53,7 @@ class TimetableWidgetConfigurePresenter @Inject constructor( disposable.add(studentRepository.getSavedStudents(false) .map { it to appWidgetId?.let { id -> sharedPref.getLong(getStudentWidgetKey(id), 0) } } .map { (students, currentStudentId) -> - students.map { student -> TimetableWidgetConfigureItem(student, student.id == currentStudentId) } + students.map { student -> student to (student.id == currentStudentId) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) @@ -64,7 +61,7 @@ class TimetableWidgetConfigurePresenter @Inject constructor( when { it.isEmpty() -> view?.openLoginView() it.size == 1 && !isFromProvider -> { - selectedStudent = it.single().student + selectedStudent = it.single().first view?.showThemeDialog() } else -> view?.updateData(it) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt index 7cac892d..056225ab 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.timetablewidget +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.ui.base.BaseView interface TimetableWidgetConfigureView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List>) fun updateTimetableWidget(widgetId: Int) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index fb8c5958..dd223ad8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -18,9 +18,9 @@ import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.data.repositories.timetable.TimetableRepository +import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getCurrentThemeWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getDateWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey -import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getThemeWidgetKey import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toFormattedString @@ -41,9 +41,7 @@ class TimetableWidgetFactory( private var lessons = emptyList() - private var savedTheme: Long? = null - - private var layoutId: Int? = null + private var savedCurrentTheme: Long? = null private var primaryColor: Int? = null @@ -71,28 +69,32 @@ class TimetableWidgetFactory( val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0) updateTheme(appWidgetId) - updateLessons(date, studentId) } } private fun updateTheme(appWidgetId: Int) { - savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - layoutId = if (savedTheme == 0L) R.layout.item_widget_timetable else R.layout.item_widget_timetable_dark + savedCurrentTheme = sharedPref.getLong(getCurrentThemeWidgetKey(appWidgetId), 0) - primaryColor = if (savedTheme == 0L) R.color.colorPrimary else R.color.colorPrimaryLight - textColor = if (savedTheme == 0L) android.R.color.black else android.R.color.white - timetableChangeColor = if (savedTheme == 0L) R.color.timetable_change_dark else R.color.timetable_change_light + if (savedCurrentTheme == 0L) { + primaryColor = R.color.colorPrimary + textColor = android.R.color.black + timetableChangeColor = R.color.timetable_change_dark + } else { + primaryColor = R.color.colorPrimaryLight + textColor = android.R.color.white + timetableChangeColor = R.color.timetable_change_light + } } private fun getItemLayout(lesson: Timetable): Int { return when { prefRepository.showWholeClassPlan == "small" && !lesson.isStudentPlan -> { - if (savedTheme == 0L) R.layout.item_widget_timetable_small + if (savedCurrentTheme == 0L) R.layout.item_widget_timetable_small else R.layout.item_widget_timetable_small_dark } - savedTheme == 0L -> R.layout.item_widget_timetable - else -> R.layout.item_widget_timetable_dark + savedCurrentTheme == 1L -> R.layout.item_widget_timetable_dark + else -> R.layout.item_widget_timetable } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index 62192a1b..79888f83 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -13,6 +13,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.res.Configuration import android.widget.RemoteViews import dagger.android.AndroidInjection import io.github.wulkanowy.R @@ -71,6 +72,8 @@ class TimetableWidgetProvider : BroadcastReceiver() { fun getStudentWidgetKey(appWidgetId: Int) = "timetable_widget_student_$appWidgetId" fun getThemeWidgetKey(appWidgetId: Int) = "timetable_widget_theme_$appWidgetId" + + fun getCurrentThemeWidgetKey(appWidgetId: Int) = "timetable_widget_current_theme_$appWidgetId" } override fun onReceive(context: Context, intent: Intent) { @@ -110,14 +113,23 @@ class TimetableWidgetProvider : BroadcastReceiver() { with(sharedPref) { delete(getStudentWidgetKey(appWidgetId)) delete(getDateWidgetKey(appWidgetId)) + delete(getThemeWidgetKey(appWidgetId)) + delete(getCurrentThemeWidgetKey(appWidgetId)) } } } @SuppressLint("DefaultLocale") private fun updateWidget(context: Context, appWidgetId: Int, date: LocalDate, student: Student?) { - val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val layoutId = if (savedTheme == 0L) R.layout.widget_timetable else R.layout.widget_timetable_dark + val savedConfigureTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) + val isSystemDarkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + var currentTheme = 0L + var layoutId = R.layout.widget_timetable + + if (savedConfigureTheme == 1L || (savedConfigureTheme == 2L && isSystemDarkMode)) { + currentTheme = 1L + layoutId = R.layout.widget_timetable_dark + } val nextNavIntent = createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT) val prevNavIntent = createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV) @@ -150,7 +162,11 @@ class TimetableWidgetProvider : BroadcastReceiver() { setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) } - sharedPref.putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) + with(sharedPref) { + putLong(getCurrentThemeWidgetKey(appWidgetId), currentTheme) + putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) + } + with(appWidgetManager) { notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList) updateAppWidget(appWidgetId, remoteView) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt new file mode 100644 index 00000000..b0b6999e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.ui.widgets + +import android.content.Context +import android.graphics.Canvas +import android.view.View +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView + +class DividerItemDecoration(context: Context) : DividerItemDecoration(context, VERTICAL) { + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + canvas.save() + val dividerLeft = parent.paddingLeft + val dividerRight = parent.width - parent.paddingRight + val childCount = parent.childCount + + for (i in 0..childCount - 2) { + val child: View = parent.getChildAt(i) + val params = child.layoutParams as RecyclerView.LayoutParams + val dividerTop: Int = child.bottom + params.bottomMargin + val dividerBottom = dividerTop + drawable!!.intrinsicHeight + drawable?.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom) + drawable?.draw(canvas) + } + canvas.restore() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index a444da6f..6912baa3 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -4,7 +4,6 @@ import android.content.res.Resources import android.os.Build.MANUFACTURER import android.os.Build.MODEL import android.os.Build.VERSION.SDK_INT -import io.github.wulkanowy.BuildConfig.CRASHLYTICS_ENABLED import io.github.wulkanowy.BuildConfig.DEBUG import io.github.wulkanowy.BuildConfig.VERSION_CODE import io.github.wulkanowy.BuildConfig.VERSION_NAME @@ -13,8 +12,6 @@ import javax.inject.Singleton @Singleton open class AppInfo { - open val isCrashlyticsEnabled get() = CRASHLYTICS_ENABLED - open val isDebug get() = DEBUG open val versionCode get() = VERSION_CODE diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index 582b1c83..489e7e6f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -9,6 +9,8 @@ import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import io.github.wulkanowy.R @ColorInt fun Context.getThemeAttrColor(@AttrRes colorAttr: Int): Int { @@ -20,6 +22,11 @@ fun Context.getThemeAttrColor(@AttrRes colorAttr: Int): Int { } } +@ColorInt +fun Context.getThemeAttrColor(@AttrRes colorAttr: Int, alpha: Int): Int { + return ColorUtils.setAlphaComponent(getThemeAttrColor(colorAttr), alpha) +} + @ColorInt fun Context.getCompatColor(@ColorRes colorRes: Int) = ContextCompat.getColor(this, colorRes) diff --git a/app/src/main/java/io/github/wulkanowy/utils/FlexibleAdapterExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FlexibleAdapterExtension.kt deleted file mode 100644 index bd6867a3..00000000 --- a/app/src/main/java/io/github/wulkanowy/utils/FlexibleAdapterExtension.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.wulkanowy.utils - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem - -inline fun FlexibleAdapter<*>.setOnItemClickListener(crossinline listener: (item: AbstractFlexibleItem<*>) -> Unit) { - addListener(FlexibleAdapter.OnItemClickListener { _, position -> - listener(getItem(position) as AbstractFlexibleItem<*>) - true - }) -} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt new file mode 100644 index 00000000..2c49ee97 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt @@ -0,0 +1,55 @@ +package io.github.wulkanowy.utils + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class LifecycleAwareVariable : ReadWriteProperty, LifecycleObserver { + + private var _value: T? = null + + override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { + thisRef.viewLifecycleOwner.lifecycle.removeObserver(this) + _value = value + thisRef.viewLifecycleOwner.lifecycle.addObserver(this) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>) = _value + ?: throw IllegalStateException("Trying to call an lifecycle-aware value outside of the view lifecycle, or the value has not been initialized") + + @Suppress("unused") + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun onDestroyView() { + _value = null + } +} + +class LifecycleAwareVariableActivity : ReadWriteProperty, LifecycleObserver { + + private var _value: T? = null + + override fun setValue(thisRef: AppCompatActivity, property: KProperty<*>, value: T) { + thisRef.lifecycle.removeObserver(this) + _value = value + thisRef.lifecycle.addObserver(this) + } + + override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>) = _value + ?: throw IllegalStateException("Trying to call an lifecycle-aware value outside of the view lifecycle, or the value has not been initialized") + + @Suppress("unused") + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun onDestroyView() { + _value = null + } +} + + +@Suppress("unused") +fun Fragment.lifecycleAwareVariable() = LifecycleAwareVariable() + +fun AppCompatActivity.lifecycleAwareVariable() = LifecycleAwareVariableActivity() diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt index a91f823f..8d022fc5 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt @@ -4,9 +4,13 @@ import org.threeten.bp.DayOfWeek.FRIDAY import org.threeten.bp.DayOfWeek.MONDAY import org.threeten.bp.DayOfWeek.SATURDAY import org.threeten.bp.DayOfWeek.SUNDAY +import org.threeten.bp.Instant.ofEpochMilli import org.threeten.bp.LocalDate import org.threeten.bp.LocalDateTime +import org.threeten.bp.LocalDateTime.ofInstant import org.threeten.bp.Month +import org.threeten.bp.ZoneId +import org.threeten.bp.ZoneOffset import org.threeten.bp.format.DateTimeFormatter.ofPattern import org.threeten.bp.format.TextStyle.FULL_STANDALONE import org.threeten.bp.temporal.TemporalAdjusters.firstInMonth @@ -18,6 +22,10 @@ private const val DATE_PATTERN = "dd.MM.yyyy" fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate = LocalDate.parse(this, ofPattern(format)) +fun LocalDateTime.toTimestamp() = atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toInstant().toEpochMilli() + +fun Long.toLocalDateTime() = ofInstant(ofEpochMilli(this), ZoneId.systemDefault()) + fun LocalDate.toFormattedString(format: String = DATE_PATTERN): String = format(ofPattern(format)) fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = format(ofPattern(format)) diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt new file mode 100644 index 00000000..ccb2afeb --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt @@ -0,0 +1,29 @@ +package io.github.wulkanowy.utils + +import io.github.wulkanowy.data.db.entities.Timetable +import org.threeten.bp.Duration +import org.threeten.bp.Duration.between +import org.threeten.bp.LocalDateTime +import org.threeten.bp.LocalDateTime.now + +fun Timetable.isShowTimeUntil(previousLessonEnd: LocalDateTime?) = when { + !isStudentPlan -> false + canceled -> false + now().isAfter(start) -> false + previousLessonEnd != null && now().isBefore(previousLessonEnd) -> false + else -> between(now(), start) <= Duration.ofMinutes(60) +} + +inline val Timetable.left: Duration? + get() = when { + canceled -> null + !isStudentPlan -> null + end.isAfter(now()) && start.isBefore(now()) -> between(now(), end) + else -> null + } + +inline val Timetable.until: Duration + get() = between(now(), start) + +inline val Timetable.isJustFinished: Boolean + get() = end.isBefore(now()) && end.plusSeconds(15).isAfter(now()) && !canceled diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 6e72dd82..427ad4dc 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,10 @@ -- naprawiliśmy wyświetlanie przycisku oznaczania zadania domowego jako wykonanego -- naprawiliśmy rzadki błąd ze stabilnością przy wysyłaniu wiadomości -- naprawiliśmy błąd po logowaniu w domyślnym trybie, jeśli wcześniej użytkownik zalogowany był w trybie Mobilnego API -- ulepszyliśmy wygląd okienka ze szczegółami błędu +Wersja 0.18.0 +- naprawiliśmy odświeżanie zadań domowych +- naprawiliśmy powiadomienia na androidzie 8.0 +- oceny powinny się teraz odświeżać trochę szybciej +- dodaliśmy tryb pełnoekranowy w zadaniach +- dodaliśmy wyszukiwanie w wiadomościach +- dodaliśmy opcje oznaczania bieżącej lekcji na planie/w powiadomieniu (domyślnie wyłączone) +- dodaliśmy testową opcję naprawy powiadomień na np. Huawei, Xiaomi (znajdziesz ją w ustawieniach) Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases diff --git a/app/src/main/res/drawable-hdpi/ic_stat_timetable.png b/app/src/main/res/drawable-hdpi/ic_stat_timetable.png new file mode 100644 index 00000000..201419d5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_timetable.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_timetable.png b/app/src/main/res/drawable-mdpi/ic_stat_timetable.png new file mode 100644 index 00000000..dcfe95f2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_timetable.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_timetable.png b/app/src/main/res/drawable-xhdpi/ic_stat_timetable.png new file mode 100644 index 00000000..7264bd92 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_timetable.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png b/app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png new file mode 100644 index 00000000..1fb37b09 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png new file mode 100644 index 00000000..a95cc4f5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png differ diff --git a/app/src/main/res/drawable/background_timetable_time_left.xml b/app/src/main/res/drawable/background_timetable_time_left.xml new file mode 100644 index 00000000..0f332611 --- /dev/null +++ b/app/src/main/res/drawable/background_timetable_time_left.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_fullscreen.xml b/app/src/main/res/drawable/ic_fullscreen.xml new file mode 100644 index 00000000..86b7649b --- /dev/null +++ b/app/src/main/res/drawable/ic_fullscreen.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_fullscreen_exit.xml b/app/src/main/res/drawable/ic_fullscreen_exit.xml new file mode 100644 index 00000000..bb7140f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_fullscreen_exit.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 00000000..cd9985cb --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d07dbbd8..2ea0a4d3 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,6 @@ @@ -8,7 +9,8 @@ android:id="@+id/mainToolbar" style="@style/Widget.MaterialComponents.Toolbar.Surface" android:layout_width="match_parent" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content" + app:contentInsetStartWithNavigation="0dp" /> diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 556bdfd0..6747b7d0 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -1,10 +1,12 @@ + android:layout_height="match_parent" + tools:listitem="@layout/item_about" /> diff --git a/app/src/main/res/layout/fragment_creator.xml b/app/src/main/res/layout/fragment_contributor.xml similarity index 100% rename from app/src/main/res/layout/fragment_creator.xml rename to app/src/main/res/layout/fragment_contributor.xml diff --git a/app/src/main/res/layout/fragment_grade_statistics.xml b/app/src/main/res/layout/fragment_grade_statistics.xml index ecc2e3e0..b3fcac44 100644 --- a/app/src/main/res/layout/fragment_grade_statistics.xml +++ b/app/src/main/res/layout/fragment_grade_statistics.xml @@ -12,7 +12,7 @@ android:layout_height="wrap_content" android:background="?android:windowBackground" android:padding="5dp" - android:visibility="invisible" + android:visibility="gone" tools:ignore="UnusedAttribute" tools:listitem="@layout/item_attendance_summary" tools:visibility="visible"> @@ -59,9 +59,7 @@ android:orientation="horizontal" android:paddingStart="16dp" android:paddingTop="5dp" - android:paddingEnd="16dp" - android:visibility="invisible" - tools:visibility="visible"> + android:paddingEnd="16dp"> @@ -151,6 +152,7 @@ android:autofillHints="password" android:imeActionLabel="@string/login_sign_in" android:imeOptions="actionDone" + android:importantForAutofill="yes" android:inputType="textPassword" android:maxLines="1" app:fontFamily="sans-serif" diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index ba261e09..e4a5fab7 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -134,6 +134,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:autofillHints="username|emailAddress" + android:importantForAutofill="yes" android:inputType="textEmailAddress" android:maxLines="1" tools:targetApi="o" /> @@ -167,6 +168,7 @@ android:autofillHints="password" android:imeActionLabel="@string/login_sign_in" android:imeOptions="actionDone" + android:importantForAutofill="yes" android:inputType="textPassword" android:maxLines="1" app:fontFamily="sans-serif" diff --git a/app/src/main/res/layout/header_exam.xml b/app/src/main/res/layout/header_exam.xml index 3a2e38c5..43f089f7 100644 --- a/app/src/main/res/layout/header_exam.xml +++ b/app/src/main/res/layout/header_exam.xml @@ -10,7 +10,7 @@ android:paddingTop="10dp" android:paddingRight="20dp" android:paddingBottom="10dp" - tools:context=".ui.modules.exam.ExamHeader"> + tools:context=".ui.modules.exam.ExamAdapter"> diff --git a/app/src/main/res/layout/header_grade_details.xml b/app/src/main/res/layout/header_grade_details.xml index 75e01745..f2ba9a8c 100644 --- a/app/src/main/res/layout/header_grade_details.xml +++ b/app/src/main/res/layout/header_grade_details.xml @@ -1,81 +1,93 @@ - + android:background="?android:windowBackground"> - + android:background="?selectableItemBackground" + tools:context=".ui.modules.grade.details.GradeDetailsAdapter"> - + - + - + - - + + + + + + + diff --git a/app/src/main/res/layout/header_homework.xml b/app/src/main/res/layout/header_homework.xml index 207fcdb4..e1c6608f 100644 --- a/app/src/main/res/layout/header_homework.xml +++ b/app/src/main/res/layout/header_homework.xml @@ -10,7 +10,7 @@ android:paddingTop="10dp" android:paddingRight="20dp" android:paddingBottom="10dp" - tools:context=".ui.modules.homework.HomeworkHeader"> + tools:context=".ui.modules.homework.HomeworkAdapter"> diff --git a/app/src/main/res/layout/item_about.xml b/app/src/main/res/layout/item_about.xml index b5dc3758..f988c47b 100644 --- a/app/src/main/res/layout/item_about.xml +++ b/app/src/main/res/layout/item_about.xml @@ -4,17 +4,16 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="72dp" - android:background="?selectableItemBackground"> + android:background="?selectableItemBackground" + tools:context=".ui.modules.about.AboutAdapter"> @@ -24,9 +23,7 @@ android:layout_width="wrap_content" android:layout_height="32dp" android:layout_marginEnd="16dp" - android:layout_marginRight="16dp" android:layout_toEndOf="@id/aboutItemImage" - android:layout_toRightOf="@id/aboutItemImage" android:ellipsize="end" android:gravity="bottom" android:maxLines="1" @@ -39,13 +36,11 @@ android:layout_height="20dp" android:layout_below="@id/aboutItemTitle" android:layout_marginEnd="16dp" - android:layout_marginRight="16dp" android:layout_toEndOf="@id/aboutItemImage" - android:layout_toRightOf="@id/aboutItemImage" android:ellipsize="end" android:gravity="bottom" android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="14sp" tools:text="@tools:sample/lorem" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml index 309be812..614e4d2d 100644 --- a/app/src/main/res/layout/item_account.xml +++ b/app/src/main/res/layout/item_account.xml @@ -1,5 +1,5 @@ - + tools:context=".ui.modules.account.AccountAdapter"> - + diff --git a/app/src/main/res/layout/item_attendance.xml b/app/src/main/res/layout/item_attendance.xml index 56bcab44..6917c6ce 100644 --- a/app/src/main/res/layout/item_attendance.xml +++ b/app/src/main/res/layout/item_attendance.xml @@ -11,7 +11,7 @@ android:paddingEnd="12dp" android:paddingRight="12dp" android:paddingBottom="7dp" - tools:context=".ui.modules.attendance.AttendanceItem"> + tools:context=".ui.modules.attendance.AttendanceAdapter"> + tools:text="5" + tools:visibility="visible" /> + android:orientation="vertical" + tools:context=".ui.modules.attendance.summary.AttendanceSummaryAdapter"> @@ -46,7 +46,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/attendance_present" android:textSize="14sp" /> @@ -57,9 +56,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="50" /> @@ -77,7 +74,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/attendance_absence_unexcused" android:textSize="14sp" /> @@ -88,9 +84,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="0" /> @@ -108,7 +102,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/attendance_absence_excused" android:textSize="14sp" /> @@ -119,9 +112,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="25" /> @@ -139,7 +130,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/attendance_absence_school" android:textSize="14sp" /> @@ -150,9 +140,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="0" /> @@ -170,7 +158,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/attendance_exemption" android:textSize="14sp" /> @@ -181,9 +168,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="6" /> @@ -201,7 +186,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/attendance_unexcused_lateness" android:textSize="14sp" /> @@ -212,9 +196,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="0" /> @@ -232,7 +214,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/attendance_excused_lateness" android:textSize="14sp" /> @@ -243,9 +224,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="0" /> diff --git a/app/src/main/res/layout/item_completed_lesson.xml b/app/src/main/res/layout/item_completed_lesson.xml index 8b49ba76..b9beec80 100644 --- a/app/src/main/res/layout/item_completed_lesson.xml +++ b/app/src/main/res/layout/item_completed_lesson.xml @@ -3,16 +3,12 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/ic_all_divider" - android:foreground="?selectableItemBackground" + android:background="?selectableItemBackground" android:paddingStart="8dp" - android:paddingLeft="8dp" android:paddingTop="6dp" android:paddingEnd="12dp" - android:paddingRight="12dp" android:paddingBottom="6dp" - tools:context=".ui.modules.timetable.completed.CompletedLessonItem" - tools:ignore="UnusedAttribute"> + tools:context=".ui.modules.timetable.completed.CompletedLessonsAdapter"> + android:minHeight="56dp" + android:orientation="vertical" + tools:context=".ui.modules.about.contributor.ContributorAdapter"> + android:layout_marginBottom="8dp" + android:contentDescription="@string/contributor_avatar_description" /> + tools:text="@tools:sample/lorem" /> diff --git a/app/src/main/res/layout/item_exam.xml b/app/src/main/res/layout/item_exam.xml index 459a7ecb..bd1d4731 100644 --- a/app/src/main/res/layout/item_exam.xml +++ b/app/src/main/res/layout/item_exam.xml @@ -4,14 +4,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - tools:context=".ui.modules.exam.ExamItem"> + tools:context=".ui.modules.exam.ExamAdapter"> @@ -22,7 +21,6 @@ android:layout_height="wrap_content" android:layout_below="@id/examItemSubject" android:layout_alignStart="@id/examItemSubject" - android:layout_alignLeft="@id/examItemSubject" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:textSize="13sp" @@ -34,15 +32,11 @@ android:layout_height="wrap_content" android:layout_below="@id/examItemSubject" android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginTop="5dp" android:layout_marginEnd="20dp" - android:layout_marginRight="20dp" android:layout_marginBottom="10dp" android:layout_toEndOf="@id/examItemType" - android:layout_toRightOf="@id/examItemType" android:gravity="end" android:textSize="13sp" tools:text="@tools:sample/lorem" /> diff --git a/app/src/main/res/layout/item_grade_details.xml b/app/src/main/res/layout/item_grade_details.xml index d6d99d9b..ccc968c0 100644 --- a/app/src/main/res/layout/item_grade_details.xml +++ b/app/src/main/res/layout/item_grade_details.xml @@ -1,82 +1,82 @@ - + android:background="?android:windowBackground"> - - - + android:background="?selectableItemBackground" + android:paddingStart="12dp" + android:paddingTop="7dp" + android:paddingEnd="12dp" + android:paddingBottom="7dp" + tools:context=".ui.modules.grade.details.GradeDetailsAdapter"> - + - + - - + + + + + + + + diff --git a/app/src/main/res/layout/item_grade_summary.xml b/app/src/main/res/layout/item_grade_summary.xml index a3a49ab6..85a78571 100644 --- a/app/src/main/res/layout/item_grade_summary.xml +++ b/app/src/main/res/layout/item_grade_summary.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - tools:context=".ui.modules.grade.summary.GradeSummaryItem"> + tools:context=".ui.modules.grade.summary.GradeSummaryAdapter"> @@ -48,7 +47,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/grade_summary_points" android:textSize="14sp" /> @@ -59,9 +57,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="123/150" /> @@ -79,7 +75,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/grade_summary_predicted_grade" android:textSize="14sp" /> @@ -90,9 +85,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="5" /> @@ -110,7 +103,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="20dp" - android:layout_marginLeft="20dp" android:layout_weight="1" android:text="@string/grade_summary_final_grade" android:textSize="14sp" /> @@ -121,9 +113,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="25dp" - android:layout_marginRight="25dp" android:gravity="end" android:textSize="12sp" tools:text="5" /> diff --git a/app/src/main/res/layout/item_homework.xml b/app/src/main/res/layout/item_homework.xml index 62566e9c..89c3c688 100644 --- a/app/src/main/res/layout/item_homework.xml +++ b/app/src/main/res/layout/item_homework.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - tools:context=".ui.modules.homework.HomeworkItem"> + tools:context=".ui.modules.homework.HomeworkAdapter"> + + + + + android:paddingRight="16dp" + tools:context=".ui.modules.about.license.LicenseAdapter"> + tools:context=".ui.modules.login.studentselect.LoginStudentSelectAdapter"> diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index 4379c2fc..111de88c 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -9,14 +9,13 @@ android:paddingTop="10dp" android:paddingRight="16dp" android:paddingBottom="10dp" - tools:context=".ui.modules.message.MessageItem"> + tools:context=".ui.modules.message.tab.MessageTabAdapter"> + tools:context=".ui.modules.mobiledevice.MobileDeviceAdapter"> + tools:context=".ui.modules.more.MoreAdapter"> diff --git a/app/src/main/res/layout/item_note.xml b/app/src/main/res/layout/item_note.xml index 3a56f9de..660f32e0 100644 --- a/app/src/main/res/layout/item_note.xml +++ b/app/src/main/res/layout/item_note.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - tools:context=".ui.modules.note.NoteItem"> + tools:context=".ui.modules.note.NoteAdapter"> + tools:context=".ui.modules.schoolandteachers.teacher.TeacherAdapter"> + tools:context=".ui.modules.timetable.TimetableAdapter"> @@ -86,35 +80,78 @@ android:id="@+id/timetableItemTeacher" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignBottom="@+id/timetableItemNumber" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginEnd="16dp" - android:layout_marginRight="16dp" - android:layout_toEndOf="@id/timetableItemRoom" - android:layout_toRightOf="@id/timetableItemRoom" - android:maxLines="1" android:ellipsize="end" + android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="13sp" + app:layout_constraintBottom_toBottomOf="@+id/timetableItemNumber" + app:layout_constraintStart_toEndOf="@id/timetableItemRoom" tools:text="Agata Kowalska - Błaszczyk" tools:visibility="gone" /> - + + + + + + diff --git a/app/src/main/res/layout/item_timetable_small.xml b/app/src/main/res/layout/item_timetable_small.xml index 7a5ab033..98a213ec 100644 --- a/app/src/main/res/layout/item_timetable_small.xml +++ b/app/src/main/res/layout/item_timetable_small.xml @@ -3,14 +3,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - tools:context=".ui.modules.timetable.TimetableItem"> + tools:context=".ui.modules.timetable.TimetableAdapter"> @@ -37,9 +34,7 @@ android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_toEndOf="@+id/timetableSmallItemTimeStart" - android:layout_toRightOf="@+id/timetableSmallItemTimeStart" android:ellipsize="end" android:maxLines="1" android:textColor="?android:textColorPrimary" @@ -51,10 +46,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginTop="1dp" android:layout_toEndOf="@+id/timetableSmallItemSubject" - android:layout_toRightOf="@+id/timetableSmallItemSubject" android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="13sp" @@ -65,10 +58,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginTop="1dp" android:layout_toEndOf="@id/timetableSmallItemRoom" - android:layout_toRightOf="@id/timetableSmallItemRoom" android:ellipsize="end" android:maxLines="1" android:textColor="?android:textColorSecondary" @@ -80,12 +71,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:layout_marginTop="1dp" android:layout_marginEnd="16dp" - android:layout_marginRight="16dp" android:layout_toEndOf="@id/timetableSmallItemTeacher" - android:layout_toRightOf="@id/timetableSmallItemTeacher" android:ellipsize="end" android:singleLine="true" android:textColor="?android:textColorSecondary" diff --git a/app/src/main/res/layout/item_widget_timetable.xml b/app/src/main/res/layout/item_widget_timetable.xml index c69bade8..33f686da 100644 --- a/app/src/main/res/layout/item_widget_timetable.xml +++ b/app/src/main/res/layout/item_widget_timetable.xml @@ -12,12 +12,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" - android:paddingTop="6dp" - android:paddingBottom="6dp" - android:paddingLeft="6dp" android:paddingStart="6dp" + android:paddingLeft="6dp" + android:paddingTop="6dp" android:paddingEnd="12dp" - android:paddingRight="12dp"> + android:paddingRight="12dp" + android:paddingBottom="6dp"> + tools:visibility="gone" /> + android:paddingBottom="6dp"> + tools:visibility="gone" /> + + android:orientation="vertical" + tools:context=".ui.modules.about.AboutAdapter"> + android:orientation="vertical" + tools:context=".ui.modules.attendance.summary.AttendanceSummaryAdapter"> + android:orientation="horizontal" + tools:context=".ui.modules.grade.summary.GradeSummaryAdapter"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/widget_timetable.xml b/app/src/main/res/layout/widget_timetable.xml index aa2269ad..8a08b5d2 100644 --- a/app/src/main/res/layout/widget_timetable.xml +++ b/app/src/main/res/layout/widget_timetable.xml @@ -16,9 +16,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toStartOf="@id/timetableWidgetAccount" - android:layout_toLeftOf="@id/timetableWidgetAccount" android:layout_toEndOf="@id/timetableWidgetNext" - android:layout_toRightOf="@id/timetableWidgetNext" android:orientation="vertical"> @@ -60,7 +55,6 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:backgroundTint="@color/colorPrimaryDark" android:contentDescription="@string/all_prev" android:src="@drawable/ic_widget_chevron" @@ -72,7 +66,6 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toEndOf="@id/timetableWidgetPrev" - android:layout_toRightOf="@id/timetableWidgetPrev" android:backgroundTint="@color/colorPrimaryDark" android:contentDescription="@string/all_next" android:rotation="180" diff --git a/app/src/main/res/layout/widget_timetable_dark.xml b/app/src/main/res/layout/widget_timetable_dark.xml index 3a301eb9..5533eaee 100644 --- a/app/src/main/res/layout/widget_timetable_dark.xml +++ b/app/src/main/res/layout/widget_timetable_dark.xml @@ -16,9 +16,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toStartOf="@id/timetableWidgetAccount" - android:layout_toLeftOf="@id/timetableWidgetAccount" android:layout_toEndOf="@id/timetableWidgetNext" - android:layout_toRightOf="@id/timetableWidgetNext" android:orientation="vertical"> @@ -60,7 +55,6 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginStart="10dp" - android:layout_marginLeft="10dp" android:backgroundTint="@color/colorWidgetNavButton" android:contentDescription="@string/all_prev" android:src="@drawable/ic_widget_chevron" @@ -72,7 +66,6 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toEndOf="@id/timetableWidgetPrev" - android:layout_toRightOf="@id/timetableWidgetPrev" android:backgroundTint="@color/colorWidgetNavButton" android:contentDescription="@string/all_next" android:rotation="180" diff --git a/app/src/main/res/menu/action_menu_message_tab.xml b/app/src/main/res/menu/action_menu_message_tab.xml new file mode 100644 index 00000000..18d34875 --- /dev/null +++ b/app/src/main/res/menu/action_menu_message_tab.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 8eae8419..28ad08bc 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -1,5 +1,18 @@ + + Licht + Dunkel + Schwarz (AMOLED) + + + System Sprache + Polski + English + Pусский + Українська + Deutsch + 15 Minuten 30 Minuten @@ -9,22 +22,6 @@ 12 Stunden 24 Stunden - - - Licht - Dunkel - Schwarz (AMOLED) - - - - System Sprache - Polski - English - Pусский - Українська - Deutsch - - 0,0 0,25 @@ -32,18 +29,15 @@ 0,5 0,75 - Dzienniczek+ Wulkanowy Farben der Bewertungen im Logbuch - Durchschnittsnote für das 2. Semester Durchschnitt der Bewertungen für das ganze Jahr - Nicht zeigen Alle zeigen diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 239c4043..f92186c4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,8 +1,6 @@ Wulkanowy - - Anmelden Wulkanowy @@ -21,11 +19,8 @@ Eintragen und Erfolgen Hausaufgaben Wählen Sie ein Konto - Semester %d, %d/%d - - Melden Sie sich mit dem Studenten- oder Elternkonto an Geben Sie das Symbol @@ -53,27 +48,28 @@ Student nicht gefunden. Überprüfen Sie das Symbol Dieses Datenfeld ist erforderlich Ausgewählter Student ist bereits angemeldet. - Das Symbol finden Sie auf der Registerseite unter Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne + Das Symbol finden Sie auf der Registerseite unter Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne Wählen Sie die Studenten aus, die sich bei der Anwendung anmelden sollen. Andere Optionen + In diesem Modus funktioniert eine Glücknummer, eine Klassenstatistik, eine Zusammenfassung der Anwesenheit, eine Entschuldigung für die Abwesenheit, abgeschlossene Lektionen, Schulinformationen und eine Vorschau der Liste der registrierten Geräte nicht + In diesem Modus werden dieselben Daten angezeigt, die auf der Klassenbuch-Website angezeigt werden + Die Kombination der besten Eigenschaften der beiden anderen Modus. Es arbeitet schneller als Scraper und bietet Funktionen, die im mobilen API-Modus nicht verfügbar sind. Es ist in der experimentellen Phase Datenschutzerklärung Probleme bei der Anmeldung? Kontaktieren Sie uns! Email Discord email senden + Beschreiben Sie die Details des Problems: + Stellen Sie sicher, dass das richtige UONET + Klassenbuch ausgewählt ist! Ich habe mein Passwort vergessen. Ihr Konto wiederherstellen Wiederherstellen Student ist bereits angemeldet - - Kundenbetreuer Anmelden Die Sitzung ist abgelaufen Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein - - Note Semester %d @@ -112,8 +108,6 @@ Du hast %1$d Note bekommen Du hast %1$d Noten bekommen - - Lektion Klassenzimmer @@ -121,17 +115,21 @@ Stunden Änderungen Kein Unterricht an diesem Tag - - - + %s min + %s sek + noch %1$s + in %1$s + Fertig + Jetzt: %s + In einem Moment: %s + Später: %s + Beendete Lektionen Beendete Lektionen anzeigen Keine Informationen über beendete Lektionen Thema Abwesenheit Ressourcen - - Übersicht über die Schulbesuch Aus schulischen Gründen abwesend @@ -152,19 +150,13 @@ Abwesenheit erfolgreich entschuldigt! Sie müssen mindestens eine Abwesenheit auswählen! Verzeihung - - Schulbesuch Gesamt - - Diese Woche keine Prüfungen Form Eintrittsdatum - - Posteingang Gesendet @@ -198,8 +190,6 @@ Du hast %1$d nachricht bekommen Du hast %1$d nachrichten bekommen - - Keine Informationen über Eintragen Punkte @@ -215,23 +205,17 @@ Du hast %1$d Eintrag bekommen Du hast %1$d Eintragen bekommen - - Keine Informationen über Hausaufgaben Gemacht Unvollständig Anhänge - - Glückliche Nummer - "Die heutige Glücksnummer ist " + Die heutige Glücksnummer ist Keine Information über die Glücksnummer. Glücksnummer für heute - "Die heutige Glücksnummer ist: " - - + Die heutige Glücksnummer ist: Mobile Geräte Keine Geräte @@ -241,12 +225,8 @@ Token Symbol PIN - - Schule und Lehrer - - Schule Keine Informationen über die Schule @@ -257,21 +237,15 @@ Name des Pädagogen Auf Karte anzeigen Rufen Sie an - - Lehrerinnen und Lehrer Keine Informationen über Lehrer Kein Thema - - Konto hinzufügen Abmelden Wollen Sie sich von einem aktiven Studenten abmelden? Abmeldung von Student - - Version der App Mitarbeiter @@ -288,21 +262,14 @@ Besuchen Sie die Website und helfen Sie bei der Entwicklung der Anwendung Lizenzen Lizenzen der in der Anwendung verwendeten Bibliotheken - - Lizenz - - - - Avatar + + Benutzerbild Sehen Sie mehr auf GitHub - Logs teilen Aktualisieren - - Inhalt Wiederhol @@ -319,15 +286,14 @@ Thema Zurück Nächste - - + Suchen + Suchen... Keine Lektionen Thema wählen Licht Dunkel - - + Systemthema Erscheinungsbild Standard Ansicht @@ -336,23 +302,35 @@ Anwesenheit in Schulbesuch zeigen Thema der Anwendung Noten erweitern + Aktuelle Lektion im Stundenplan markieren + Liste der Diagramme in Klassenbewertungen anzeigen Unterricht der ganzen Klasse anzeigen Farbschema der Noten App Sprache Benachrichtigungen Benachrichtigungen anzeigen + Benachrichtigungen über bevorstehende Lektionen anzeigen + Synchronisierungs- und Benachrichtigungsprobleme reparieren + Ihr Gerät hat möglicherweise Probleme mit der Datensynchronisierung und Benachrichtigungen.\n\nUm diese zu reparieren, fügen Sie Wulkanowy zum Autostart hinzu und deaktivieren Sie die Batterieoptimierung in den Systemeinstellungen des Geräts. + Gehe zu den Einstellungen Debug-Benachrichtigungen anzeigen Synchronisierung Automatische Aktualisierung An Feiertagen suspendiert Aktualisierungsintervall Nur Wi-Fi + Jetzt synchronisieren + Synchronisiert! + Synchronisierung fehlgeschlagen + Synchronisierung läuft + Synchronisation + Die manuelle Synchronisierung aktualisiert die App-Ansichten nicht. + \nUm die synchronisierten Daten anzuzeigen, starten Sie die App nach der Synchronisierung neu. + Andere Wert des Plus Wert des Minus Antwort mit Nachrichtenhistorie - - Neue Einträge im Klassenbuch Neue Noten @@ -360,9 +338,8 @@ Neue Nachrichten Neue Eintragen Push-Benachrichtigungen + Bevorstehende Lektionen Debuggen - - Schwarz Rot @@ -370,13 +347,9 @@ Grün Violett Keine Farbe - - Kopiert lösen - - Keine Internetverbindung Das Zeitlimit für die Verbindung zum Klassenbuch ist abgelaufen diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 49264538..a06a46f0 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -8,6 +8,8 @@ @color/colorDividerInverse ?colorSurface ?android:textColorPrimary + @android:color/black + @android:color/black false true diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 5553760a..475f7327 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -1,5 +1,18 @@ + + Jasny + Ciemny + Czarny (AMOLED) + + + Język systemu + Polski + English + Pусский + Українська + Deutsch + 15 minut 30 minut @@ -9,22 +22,6 @@ 12 godzin 24 godziny - - - Jasny - Ciemny - Czarny (AMOLED) - - - - Język systemu - Polski - English - Pусский - Українська - Deutsch - - 0,0 0,25 @@ -32,18 +29,15 @@ 0,5 0,75 - Dzienniczek+ Wulkanowy Kolory ocen w dzienniku - Średnia ocen z 2 semestru Średnia ocen z całego roku - Nie pokazuj Pokazuj wszystkie diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 742d61ed..6bf0c324 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,9 +1,6 @@ - Wulkanowy - - Logowanie Wulkanowy @@ -22,11 +19,8 @@ Uwagi i osiągnięcia Zadania domowe Wybierz konto - Semestr %d, %d/%d - - Zaloguj się za pomocą konta ucznia lub rodzica Podaj symbol @@ -35,13 +29,13 @@ Login, PESEL lub e-mail Hasło Dziennik UONET+ - Symbol Mobilne API Scraper Hybrydowe Token PIN Klucz API + Symbol Zaloguj To hasło jest za krótkie Dane logowania są niepoprawne. Upewnij się, że został wybrany odpowiedni dziennik UONET+ w polu poniżej @@ -65,20 +59,17 @@ Email Discord Wyślij email + Opisz problem: Upewnij się, że został wybrany odpowiedni dziennik UONET+! Nie pamiętam hasła Przywróć swoje konto Przywróć Uczeń jest już zalogowany - - Menadżer kont Zaloguj się Sesja wygasła Sesja wygasła, zaloguj się ponownie - - Ocena Semestr %d @@ -123,8 +114,6 @@ Masz %1$d nowych ocen Masz %1$d nowych ocen - - Lekcja Sala @@ -132,17 +121,21 @@ Godziny Zmiany Brak lekcji w tym dniu - - - + %s min + %s sek + jeszcze %1$s + za %1$s + Zakończona + Teraz: %s + Za chwilę: %s + Później: %s + Lekcje zrealizowane Zobacz lekcje zrealizowane Brak informacji o lekcjach zrealizowanych Temat Nieobecność Zasoby - - Podsumowanie frekwencji Nieobecność z przyczyn szkolnych @@ -165,19 +158,13 @@ Usprawiedliwiono pomyślnie! Musisz wybrać co najmniej jedną nieobecność! Usprawiedliw - - Frekwencja Razem - - Brak sprawdzianów w tym tygodniu Typ Data wpisu - - Odebrane Wysłane @@ -217,8 +204,6 @@ Masz %1$d nowych wiadomości Masz %1$d nowych wiadomości - - Brak informacji o uwagach Punkty @@ -240,23 +225,17 @@ Masz %1$d nowych uwag Masz %1$d nowych uwag - - Brak zadań domowych Wykonane Niewykonane Załączniki - - Szczęśliwy numerek Dzisiejszym szczęśliwym numerkiem jest Brak informacji o szczęśliwym numerku Szczęśliwy numerek na dzisiaj Dziś szczęśliwym numerkiem jest: %d - - Dostęp mobilny Brak urządzeń @@ -266,12 +245,8 @@ Token Symbol PIN - - Szkoła i nauczyciele - - Szkoła Brak informacji o szkole @@ -282,21 +257,15 @@ Imię i nazwisko pedagoga Pokaż na mapie Zadzwoń - - Nauczyciele Brak informacji o nauczycielach Brak przedmiotu - - Dodaj konto Wyloguj Czy chcesz wylogować aktualnego ucznia? Wylogowanie ucznia - - Wersja aplikacji Twórcy @@ -313,21 +282,14 @@ Odwiedź stronę i pomóż rozwijać aplikację Licencje Licencje użytych bibliotek w aplikacji - - Licencja - - - + Awatar Zobacz więcej na GitHub - Udostępnij logi Odśwież - - Treść Ponów @@ -344,15 +306,14 @@ Przedmiot Poprzedni Następny - - + Szukaj + Szukaj... Brak lekcji Wybierz motyw Jasny Ciemny - - + Motyw systemu Wygląd Domyślny widok @@ -361,14 +322,18 @@ Pokazuj obecność we frekwencji Motyw aplikacji Rozwiń oceny + Oznaczaj bieżącą lekcję na planie + Pokazuj listę wykresów w ocenach klasy Pokazuj lekcje całej klasy Schemat kolorów ocen Język aplikacji - Powiadomienia Pokazuj powiadomienia + Pokazuj powiadomienia o nadchodzących lekcjach + Napraw problemy z synchronizacją i powiadomieniami + Na twoim urządzeniu mogą występować problemy z synchronizacją danych i powiadomieniami.\n\nBy je naprawić, dodaj Wulkanowego do autostartu i wyłącz optymalizację/oszczędzanie baterii w ustawieniach systemowych telefonu. + Przejdź do ustawień Pokazuj powiadomienia debugowania - Synchronizacja Automatyczna aktualizacja Zawieszona na wakacjach @@ -379,27 +344,22 @@ Synchronizacja nie powiodła się Synchronizacja w trakcie Synchronizacja - - Ręczna synchronizacja nie odświeża widoków w aplikacji. + Ręczna synchronizacja nie odświeża widoków w aplikacji. \nAby zobaczyć zsynchronizowane informacje uruchom ponownie aplikację po zsynchronizowaniu. - Inne Wartość plusa Wartość minusa Odpowiadaj z historią wiadomości - - Nowe wpisy w dzienniku - Szczęśliwy numerek Nowe oceny + Szczęśliwy numerek Nowe wiadomości Nowe uwagi Powiadomienia push + Nadchodzące lekcje Debugowanie - - Czarny Czerwony @@ -407,13 +367,9 @@ Zielony Fioletowy Brak koloru - - Skopiowano Cofnij - - Brak połączenia z internetem Upłynął limit czasu na połączenie z dziennikiem diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 23cb5ac4..6c152268 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -1,14 +1,5 @@ - - 15 минут - 30 минут - 1 час - 2 часа - 6 часов - 12 часов - 24 часа - Светлая Тёмная @@ -22,6 +13,15 @@ Українська Deutsch + + 15 минут + 30 минут + 1 час + 2 часа + 6 часов + 12 часов + 24 часа + 0,0 0,25 @@ -38,7 +38,6 @@ Средняя оценка со 2 семестра Средняя оценка с целого года - Не показывать Показать все diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e2f13f30..c0666e2c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,9 +1,6 @@ - - + Wulkanowy - - Авторизация Wulkanowy @@ -12,21 +9,18 @@ Тесты Расписание Настройки - Ещё + Другое О приложении Просмотр журнала - Творцы + Разработчики Лицензии Сообщения Новое сообщение Предупреждения и свершения Домашние задания Выберите аккаунт - - Семестр %d, %d/%d - - + %d семестр, %d/%d Авторизируйтесь при помощи аккаунта ученика или родителя Впишите \"symbol\" @@ -35,72 +29,73 @@ Логин, PESEL или электронная почта Пароль Дневник UONET+ - Мобильный API + Mobile API Scraper - Гибрид + Hybrid Token PIN - клавиша API + Ключ API Symbol Войти Слишком короткий пароль - Указаны неверные данные - Недействительный PIN - Недействительный token + Указаны неверные данные. Убедитесь, что вы выбрали нужный дневник + Неправильный PIN + Неправильный token Токен просрочен Неверный адрес электронной почты Неправильный логин - Недействительный symbol - Не удалось найти ученика. Пожалуйста, проверьте \"symbol\" - Это поле обязательно + Неправильный symbol + Не удалось найти ученика. Проверьте \"symbol\" + Обязательное поле Данный ученик уже авторизован - Вы можете найти \"symbol\" в Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne + Вы можете найти \"symbol\" на странице VULCAN по пути Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne Выберите учеников для авторизации в приложении Другие варианты + В этом режиме не работают: счастливый номер, статистика класса по оценкам, статистика посещаемости и уроков, информация о школе и список зарегистрированных устройств + Scraper - режим, который показывает данные так же, как и сайт дневника + Hybrid - это комбинация лучших функций остальных двух режимов. Он работает быстрее, чем Scraper, и вводит функции, которых нет в режиме Mobile API. Находится в экспериментальной стадии Политика приватности Проблемы с авторизацией? Свяжитесь с нами! Электронная почта Discord Отправить письмо - восстановление - Я не помню пароль + Опишите детали проблемы: + Убедитесь, что выбран нужный дневник! + Забыли пароль? Восстановите свой аккаунт + Восстановить Студент уже вошел в систему - - Менеджер аккаунтов Войти Сеанс истёк Сеанс истёк, пожалуйста, авторизируйтесь ещё раз - - Оценка Семестр %d Сменить семестр Оценки отсутствуют - Вес - Bес: %s + Стоимость + Стоимость: %s Комментарий Новые оценки отсутствуют Количество новых оценок: %1$d Средняя оценка: %1$.2f - точек: %s + Баллы: %s Средняя оценка отсутствует Ожидаемая оценка: %1$s Итоговая оценка: %1$s - Сумма очков + Сумма баллов Итоговая оценка Ожидаемая оценка Рассчитанная средняя оценка Итоговая средняя оценка Итоги Класс - Пометить как \"прочитанное* + Пометить как \"прочитанное\" Частичные - Семестровые - Очки + За семестр + Баллы %d оценка %d оценки @@ -108,7 +103,7 @@ %d оценок - Новая оценка + Новая оценка Новые оценки Новые оценки Новые оценки @@ -119,34 +114,36 @@ Вы получили %1$d оценок Вы получили %1$d оценок - - Урок Аудитория Группа Часы Изменения - Нету уроков в данный день - - - + Нет уроков в данный день + %s min + %s sec + %1$s left + in %1$s + Finished + Now: %s + Next: %s + Later: %s + Проведённые уроки Просмотреть проведённые уроки Нет информации о проведённых уроках Тема Отсутствие Ресурсы - - Итоговая посещаемость Отсутствие по школьным причинам - Оправданное отсутствие - Неоправданное отсутствие + Отсутствие по уважительной причине + Отсутствие по неуважительной причине Освобождение - Оправданное опоздание - Неоправданное опоздание + Опоздание по уважительным причинам + Опоздание по неуважительным причинам Присутствие Урок № Данные не найдены @@ -158,29 +155,23 @@ Причина отсутствия (необязательно) Послать - Отсутствие оправдано успешно! - Вы должны выбрать хотя бы одно отсутствие! - Oбосновывать - - + Статус отсутствия изменён + Выберите хотя-бы одно отсутствие + Изменить статус Посещаемость - вместе - - + Общая - Тесты на этой неделе не запланированы + Нет тестов на этой неделе Тип Дата записи - - Полученные Отправленные Корзина - (нету темы) + (нет темы) Нет сообщений - Произошла ошибка во время получения текста сообщения + Произошла ошибка во время получения содержания сообщения От: Кому: Дата: %s @@ -202,7 +193,7 @@ %d сообщений - Новое сообщение + Новое сообщение Новые сообщения Новые сообщения Новые сообщения @@ -213,11 +204,9 @@ Вы получили %1$d новых сообщений Вы получили %1$d новых сообщений - - Нет данных о предупреждениях - точек + Баллы %d предупреждение %d предупреждения @@ -225,7 +214,7 @@ %d предупреждений - Новое предупреждение + Новое предупреждение Новые предупреждения Новых предупреждений Новых предупреждений @@ -236,23 +225,17 @@ Вы получили %1$d предупреждений Вы получили %1$d предупреждений - - Нет домашних заданий сделанный Не сделано Вложения - - Счастливый номер Сегодняшний счастливый номер Нет данных о счастливом номере Сегодняшний счастливый номер Сегодняшний счастливый номер: %d - - Мобильные устройства Нет устройств @@ -262,12 +245,8 @@ Token Symbol PIN - - Школа и учителя - - Школа Нет информации о школе @@ -278,56 +257,42 @@ Педагог Показать на карте Позвонить - - Учителя Нет информации о учителях Нет предмета - - Добавить аккаунт Выйти Вы точно хотите выйти из данного аккаунта? Выйти - - Версия приложения - Творцы - Список Wulkanowy программистов - Сообщить о ошибке - Отправить сообщение о ошибке через электронную почту + Разработчики + Список разработчиков \"Wulkanowy\" + Возникла ошибка? + Сообщить о ошибке FAQ - Читайте часто задаваемые вопросы + Часто задаваемые вопросы Сервер Discord Присоединиться к сообществу приложения Политика приватности - Правила сбора личных данных + Правила хранения личных данных Домашняя страница - Посетить страницу и помочь в развитии приложения + Помочь в развитии приложения Лицензии - Лицензии использованных в приложении библиотек - - + Лицензии использованных библиотек Лицензия - - - + Aватар - Смотрите больше на GitHub - - + Страница проекта на GitHub - Share logs - Обновление - - + Поделиться логами + Обновить Содержание - Снова + Повторить Описание Нет описания Учитель @@ -341,53 +306,60 @@ Предмет Предыдущий Следующий - - + Search + Search... Нет уроков Выбрать тему Светлая Тёмная - - + Тема системы - Внешний вид - При входе открывать - Рассчитывание средней годовой оценки + Вид + Окно по умолчанию + Способ определения средней годовой оценки Принудительно высчитать среднюю оценку через приложение Показывать присутствия в посещаемости Тема приложения Больше оценок + Mark current lesson in timetable + Show chart list in class grades Показать уроки всего класса Схема цветов оценок Язык приложения - Уведомления Показывать уведомления + Show upcoming lesson notifications + Fix synchronization & notifications issues + Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. + Go to settings Показывать дебаг-уведомления - Синхронизация Автоматическая синхронизация Приостановить синхронизации во время каникул Интервал синхронизации Только через Wi-Fi - + Синхронизировать + Синхронизировано! + Синхронизация не удалась + Идёт синхронизация + Синхронизация + Ручная синхронизация не обновляет данные в приложении. + \nЧтобы увидеть обновлённые данные, перезапустите приложение. + Другие - Вес плюса - Вес минуса - Ответить с историей сообщений - - + Стоимость плюса + Стоимость минуса + Отвечать с историей сообщений Новые данные в дневнике Новые оценки Счастливый номер Новые сообщения Новые заметки - Push-уведомления + Показывать push-уведомления + Upcoming lessons Дебаг - - Чёрный Красный @@ -395,17 +367,13 @@ Зелёный Фиолетовый Нет цвета - - Скопировано Отменить - - Нет интернет-подключения Слишком долгое ожидание соединения с дневником - Авторизация не удалась. Пожалуйста, попробуйте ещё раз или перезапустите дневник + Авторизация не удалась. Попробуйте ещё раз или перезапустите дневник Требуется смена пароля Технический перерыв в журнале UONET + продолжается. Попробуйте позже Произошла неожиданная ошибка diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index e09ae8cd..3c70c9d0 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -1,14 +1,5 @@ - - 15 хвилин - 30 хвилин - 1 година - 2 години - 6 годин - 12 годин - 24 години - Світла Темна @@ -22,6 +13,15 @@ Українська Deutsch + + 15 хвилин + 30 хвилин + 1 година + 2 години + 6 годин + 12 годин + 24 години + 0,0 0,25 @@ -38,7 +38,6 @@ Середня оцінка з 2 семестру Середня оцінка за весь рік - Не показувати Показати все diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d9db3f06..8a991812 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,9 +1,6 @@ - - + Wulkanowy - - Авторизація Wulkanowy @@ -12,83 +9,80 @@ Тести Розклад Налаштування - Ще + Інше Про додаток Переглядач журналів - Творці + Розробники Ліцензії Повідомлення Нове повідомлення Нотатки та досягнення Домашні завдання - Виберіть акаунт - - + Оберіть аккаунт - Семестр %d, %d/%d - - + %d семестр, %d/%d - Авторизуйтеся за допомогою акаунта учня або батьків + Авторизуйтеся за допомогою аккаунта учня або батька Впишіть \"symbol\" Ім\'я користувача Електронна пошта Логін, PESEL або електронна пошта Пароль Щоденник - Мобільний API + Мobile API Scraper - Гібрид + Hybrid Token PIN Ключ API Symbol - Ввійти - Дуже короткий пароль - Вказані неправильні дані - Недійсний PIN - Недійсний token + Увійти + Занадто короткий пароль + Вказані невірні дані. Впевніться, що ви увібрали потрібний щоденник + Неправильний PIN + Неправильний token + Минув термін дії токену Недійсна адреса електронної пошти - Невірний логін - Токен протермінований - Недійсний symbol + Неправильний логін + Неправильний symbol Не вдалося знайти учня. Будь ласка, перевірте \"symbol\" - Це поле обов\'язкове - Даний учень вже авторизований - Ви можете знайти \"symbol\" в Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne + Обов\'язкове поле + Даного учня вже авторизовано + Ви можете знайти \"symbol\" на сторінцi VULCAN стежкою Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne Виберіть учнів для авторизації в додатку Інші варіанти + У цьому режимі не працюють: щасливий номер, статистика класу по оцінкам, статистика відвідуваності і уроків, інформація про школу і список зареєстрованних пристроїв + Scraper - режим, котрий показує дані так само, як і сторінка щоденника + Hybrid - це комбінація найкращих функцій інших двох режимів. Він працює швидше, ніж Scraper, и впроваджує функції, котрих нема в режимі Mobile API. Знаходиться в експериментальній стадії Політика приватності Проблеми з авторизацією? Зв\'яжіться з нами! Електронна пошта Discord - Відправити листа - Я забув свій пароль - Відновіть свій акаунт + Відправити лист + Опишіть деталі помилки: + Переконайтесь, що обрано потрібний щоденник! + Забули пароль? + Відновіть свій аккаунт Відновити - Учень вже ввійшов в систему - - + Учень вже увійшов до системи - Менеджер акаунтів - Ввійти - Сесія закінчилася - Сесія закінчилася, будь ласка, авторизуйтеся знову - - + Менеджер аккаунтів + Увійти + Минув термін дії сесії + Минув термін дії сесії, авторизуйтеся знову Оцінка - Семестр %d + %d семестр Змінити семестр - Оцінки відсутні - Вага - Вага: %s + Брак оцінок + Вартість + Вартість: %s Коментар - Нові оцінки відсутні + Брак нових оцінок Кількість нових оцінок: %1$d Середня оцінка: %1$.2f точок: %s - Середня оцінка відсутня + Брак середньої оцінки Очікувана оцінка: %1$s Підсумкова оцінка: %1$s Сума балів @@ -96,9 +90,9 @@ Очікувана оцінка Розрахована середня оцінка Підсумкова середня оцінка - Підсумки + Підсумок Клас - Відмітити як \"прочитане* + Позначити як прочитане Поточні Семестрові Бали @@ -109,7 +103,7 @@ %d оцінок - Нова оцінка + Нова оцінка Нові оцінки Нові оцінки Нові оцінки @@ -118,39 +112,41 @@ Ви отримали %1$d оцінку Ви отримали %1$d оцінки Ви отримали %1$d оцінок - ви отримали %1$d оцінок + Ви отримали %1$d оцінок - - Урок Аудиторія Група Години Зміни - Цього дня уроків немає - - - - Проведені уроки - Подивитися проведені уроки - Інформації про проведені уроки немає + Брак уроків у цей день + %s min + %s sec + %1$s left + in %1$s + Finished + Now: %s + Next: %s + Later: %s + + Уроки, що відбулися + Показати уроки, що відбулися + Брак інформації о уроках, що відбулися Тема Відсутність Ресурси - - - Підсумкова відвідуваність + Підсумок відвідуваності Відсутність зі шкільних причин - Виправдана відсутність - Невиправдана відсутність + Відсутність з поважних причин + Відсутність з не поважних причин Звільнення - Виправдане запізнення - Невиправдане запізнення + Спізнення з поважних причин + Спізнення з не поважних причин Присутність - урок № - Дані не знайдені + Номер уроку + Брак записів %1$d відсутність %1$d відсутності @@ -159,43 +155,37 @@ Причина відсутності (необов’язково) Надіслати - Відсутність виправдана успішно! - Ви повинні вибрати хоча б одну відсутність! - Oбґрунтовувати - - + Змінено статус відсутності + Оберіть хоча б одну відсутність + Змінити статус Відвідуваність Загальна - - - Тести на цьому тижні не заплановані + Брак тестів у цьому тижні Тип Дата запису - - Отримані Відправлені Кошик - (нема теми) + (брак теми) Нема повідомлень - Сталася помилка під час отримання тексту повідомлення + З\'явилася помилка підчас завантаження змісту повідомлення Від: Кому: Дата: %s - Відповідь + Відповісти Переслати Видалити - Перемістити в кошик + Перемістити у кошик Видалити назавжди - Повідомлення успішно видалено + Повідомлення було успішно видалено Тема Зміст - Повідомлення успішно відправлено - Ви повинні вибрати щонайменше одного отримувача - Текст повідомлення повинен містити щонайменше 3 знаки + Повідомлення було успішно відправлено + Необхідно обрати принаймні 1 адресата + Зміст повідомлення мусить складатися принаймні з 3 знаків %d повідомлення %d повідомлення @@ -203,7 +193,7 @@ %d повідомлень - Нове повідомлення + Нове повідомлення Нові повідомлення Нові повідомлення Нові повідомлення @@ -214,123 +204,97 @@ Ви отримали %1$d нових повідомлень Ви отримали %1$d нових повідомлень - - - Немає даних про нотатки - точок + Брак інформації о зауваженнях + Бали - %d нотатка - %d нотатки - %d нотаток - %d нотатки + %d зауваження + %d зауваження + %d зауважень + %d зауважень - Нова нотатка + Нова нотатка Нові нотатки Нових нотаток Нових нотаток - Ви отримали %1$d нотатку - Ви отримали %1$d нотатки - Ви отримали %1$d нотаток - Ви отримали %1$d нотаток + %1$d нове зауваження + %1$d нових зауваження + %1$d нових зауважень + %1$d нових зауважень - - - Немає домашніх завдань - зроблений - Не зроблено - Вкладення - - + Брак домашніх завдань + Позначити як зроблене + Позначити як не зроблене + Додатки Щасливий номер - Сьогодні щасливий номер - Немає даних про щасливий номер - Сьогодні щасливий номер - Сьогодні щасливий номер: %d - - + Сьогоднішній щасливий номер + Брак інформації о щасливому номері + Сьогоднішній щасливий номер + Сьогоднішнім щасливим номером є %d Мобільні пристрої - Пристрої відсутні + Брак пристроїв Видалити - Пристрій видалений + Пристрій видалено QR-код Token Symbol PIN - - Школа та вчителі - - Школа - Немає інформації про школу + Брак інформації про школу Назва школи Адреса школи Телефон Директор Викладач - Показати на карті + Показати на мапі Зателефонувати - - Вчителі - Немає інформації про вчителів - Нема предмету - - + Брак інформації про вчителів + Брак предмету - Додати акаунт + Додати аккаунт Вийти - Ви дійсно бажаєте вийти з даного акаунту? - Вийти - - + Ви впевнені, що хочете вийти з цього аккаунту? + Вийти з аккаунту учня - Версія додатку - Tворці - Список Wulkanowy програмістів - Повідомити про помилку - Відправити повідомлення про помилку електронною поштою + Версія додатка + Розробники + Список розробників \"Wulkanowy\" + Виникла помилка? + Повідомити о помилці за допомогою e-mail FAQ - Читайте запитання, які часто задають + Запитання, які часто задають Сервер Discord Приєднатися до спільноти додатка - Політика приватності - Правила збору особистих даних + Політика конфіденційності + Правила зберігання особистих даних Домашня сторінка - Відвідати сторінку та допомогти в розвитку додатку + Допомогти розвитку додатка Ліцензії - Ліцензії використаних в додатку бібліотек - - + Ліцензії вжитих бібліотек Ліцензія - - - + Аватар - Дивіться більше на GitHub - - + Сторінка проекту на GitHub - Поділитися журналами + Поділитися логами Оновити - - Зміст - Знову + Повторити Опис - Нема опису + Брак опису Вчитель Дата Дата запису @@ -342,71 +306,74 @@ Предмет Попередній Наступний - - + Search + Search... - Немає уроків - Вибрати тему - Світла + Брак уроків + Увібрати тему + Яскрава Темна - - + Тема системи - Зовнішній вигляд - При вході відкривати - Розрахунок середньої річної оцінки + Вигляд + Вікно за замовчуванням + Спосіб облічування оцінки на кінець року Примусово розрахувати середню оцінку через додаток Показувати присутність у відвідуваності Тема додатку Більше оцінок + Mark current lesson in timetable + Show chart list in class grades Показати уроки всього класу - Колірна гама оцінок + Схема кольорів оцінок Мова додатку - Повідомлення Показувати повідомлення + Show upcoming lesson notifications + Fix synchronization & notifications issues + Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. + Go to settings Показувати дебаг-повідомлення - Синхронізація Автоматична синхронізація - Призупинити синхронізації на час канікул - Інтервал синхронізації + Призупинено на час канікул + Інтервал оновлення Тільки через Wi-Fi - + Синхронізувати + Синхронізовано! + Синхронізація не вдалася + Триває синхронізація + Синхронізація + Ручна синхронізація не оновлює дані в додатку. + \nЩоб побачити оновлені дані, перезавантажте додаток. + Інші - Вага плюса + Вартість плюсу Вага мінуса Відповісти з історією повідомлень - - Нові дані в щоденнику - Щасливий номер Нові оцінки + Щасливий номер Нові повідомлення Нові нотатки - Повідомлення pushs + Показувати push-повідомлення + Upcoming lessons Дебаг - - Чорний Червоний Голубий Зелений Фіолетовий - Нема кольору - - + Брак кольору Скопійовано Відмінити - - - Відсутнє інтернет-підключення + Брак з\'єднання з інтернетом Занадто довге очікування з\'єднання з щоденником - Авторизація не відбулася. Будь ласка, авторизуйтеся знову або перезавантажте щоденник + Аутентифікація не вдалася. Спробуйте ще раз або запустіть додаток знову Потрібно змінити пароль Технічна перерва в журналі UONET + продовжується. Спробуйте пізніше Відбулася несподівана помилка diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 5ce26b1d..f661b88c 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -36,7 +36,7 @@ https://vulcan.net.pl/ https://vulcan.net.pl/ https://vulcan.net.pl/ - http://fakelog.cf/?standard + http://fakelog.tk/?standard Default diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index be31b440..a82b14eb 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -5,6 +5,7 @@ only_one_semester false false + false light vulcan system @@ -12,9 +13,11 @@ 60 false true + false false 0.33 0.33 true no + false diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 559e2159..6cb877ec 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -7,15 +7,19 @@ expand_grade grade_average_mode grade_average_always_calc + grade_statistics_list app_language services_enable services_interval services_disable_wifi_only services_force_sync + notifications_fix_issues notifications_enable + notifications_upcoming_lessons_enable notification_debug grade_modifier_plus grade_modifier_minus fill_message_content show_whole_class_plan + timetable_show_timers diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7b41f69..6e08fded 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,7 +55,7 @@ Student not found. Check the symbol This field is required Selected student is already logged in - The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne + The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne Select students to log in to the application Other options In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices @@ -130,6 +130,14 @@ Hours Changes No lessons this day + %s min + %s sec + %1$s left + in %1$s + Finished + Now: %s + Next: %s + Later: %s @@ -329,6 +337,8 @@ Subject Prev Next + Search + Search... @@ -336,6 +346,7 @@ Choose theme Light Dark + System Theme @@ -346,12 +357,18 @@ Show presence in attendance Application theme Expand grades + Mark current lesson in timetable + Show chart list in class grades Show whole class lessons Grades color scheme App language Notifications Show notifications + Show upcoming lesson notifications + Fix synchronization & notifications issues + Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. + Go to settings Show debug notifications Synchronization @@ -382,6 +399,7 @@ New messages New notes Push notifications + Upcoming lessons Debug diff --git a/app/src/main/res/xml/scheme_preferences.xml b/app/src/main/res/xml/scheme_preferences.xml index bb9eb5fc..c05910f9 100644 --- a/app/src/main/res/xml/scheme_preferences.xml +++ b/app/src/main/res/xml/scheme_preferences.xml @@ -1,4 +1,4 @@ - + + + + app:key="@string/pref_key_services_force_sync" + app:title="@string/pref_services_force_sync" /> + + ())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true) + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) + `when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester") + `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries)) - val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) - .blockingGet() + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - assertEquals(2, averages.size) - assertEquals(2.5, averages.single { it.first == "Matematyka" }.second, .0) - assertEquals(3.0, averages.single { it.first == "Fizyka" }.second, .0) + assertEquals(2, items.size) + assertEquals(2.5, items.single { it.subject == "Matematyka" }.average, .0) + assertEquals(3.0, items.single { it.subject == "Fizyka" }.average, .0) } @Test fun onlyOneSemester_gradesWithModifiers_default() { - doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode - doReturn(Single.just(emptyList())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true) - doReturn(Single.just(secondGradeWithModifier)).`when`(gradeRepository).getGrades(student, semesters[2], true) + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) + `when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester") + `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier)) - val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) - .blockingGet() + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - assertEquals(3.5, averages.single { it.first == "Język polski" }.second, .0) + assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) } @Test fun onlyOneSemester_gradesWithModifiers_api() { - doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode - doReturn(Single.just(emptyList())).`when`(gradeSummaryRepository).getGradesSummary(student.copy(loginMode = Sdk.Mode.API.name), semesters[2], true) - doReturn(Single.just(secondGradeWithModifier)).`when`(gradeRepository).getGrades(student.copy(loginMode = Sdk.Mode.API.name), semesters[2], true) + val student = student.copy(loginMode = Sdk.Mode.API.name) - val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.API.name), semesters, semesters[2].semesterId, true) - .blockingGet() + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) + `when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester") + `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) + `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier)) - assertEquals(3.375, averages.single { it.first == "Język polski" }.second, .0) + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() + + assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0) } @Test fun onlyOneSemester_gradesWithModifiers_scrapper() { - doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode - doReturn(Single.just(emptyList())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true) - doReturn(Single.just(secondGradeWithModifier)).`when`(gradeRepository).getGrades(student.copy(loginMode = Sdk.Mode.SCRAPPER.name), semesters[2], true) + val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.SCRAPPER.name), semesters, semesters[2].semesterId, true) - .blockingGet() + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) + `when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester") + `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) + `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier)) - assertEquals(3.5, averages.single { it.first == "Język polski" }.second, .0) + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() + + assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) } @Test fun onlyOneSemester_gradesWithModifiers_hybrid() { - doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode - doReturn(Single.just(emptyList())).`when`(gradeSummaryRepository).getGradesSummary(student.copy(loginMode = Sdk.Mode.HYBRID.name), semesters[2], true) - doReturn(Single.just(secondGradeWithModifier)).`when`(gradeRepository).getGrades(student.copy(loginMode = Sdk.Mode.HYBRID.name), semesters[2], true) + val student = student.copy(loginMode = Sdk.Mode.HYBRID.name) - val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.HYBRID.name), semesters, semesters[2].semesterId, true) - .blockingGet() + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) + `when`(preferencesRepository.gradeAverageMode).thenReturn("only_one_semester") + `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) + `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier)) - assertEquals(3.375, averages.single { it.first == "Język polski" }.second, .0) + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() + + assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0) } @Test fun allYearFirstSemesterTest() { - doReturn("all_year").`when`(preferencesRepository).gradeAverageMode - doReturn(Single.just(emptyList())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[1], true) + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) + `when`(preferencesRepository.gradeAverageMode).thenReturn("all_year") + `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries)) - val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[1].semesterId, true) - .blockingGet() + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId).blockingGet() - assertEquals(2, averages.size) - assertEquals(3.5, averages.single { it.first == "Matematyka" }.second, .0) - assertEquals(3.5, averages.single { it.first == "Fizyka" }.second, .0) + assertEquals(2, items.size) + assertEquals(3.5, items.single { it.subject == "Matematyka" }.average, .0) + assertEquals(3.5, items.single { it.subject == "Fizyka" }.average, .0) } @Test fun allYearSecondSemesterTest() { - doReturn("all_year").`when`(preferencesRepository).gradeAverageMode - doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], false) - doReturn(Single.just(emptyList())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true) + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) + `when`(preferencesRepository.gradeAverageMode).thenReturn("all_year") + `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries)) + `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries)) - val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) - .blockingGet() + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - assertEquals(2, averages.size) - assertEquals(3.0, averages.single { it.first == "Matematyka" }.second, .0) - assertEquals(3.25, averages.single { it.first == "Fizyka" }.second, .0) + assertEquals(2, items.size) + assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0) + assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) } @Test(expected = IllegalArgumentException::class) fun incorrectAverageModeTest() { - doReturn("test_mode").`when`(preferencesRepository).gradeAverageMode + `when`(preferencesRepository.gradeAverageMode).thenReturn("test_mode") - gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true).blockingGet() + gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).blockingGet() } @Test - fun onlyOneSemester_averageFromSummary() { - doReturn("all_year").`when`(preferencesRepository).gradeAverageMode - doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], false) - doReturn(Single.just(listOf( - getSummary(22, "Matematyka", 3.1), - getSummary(22, "Fizyka", 3.26) - ))).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true) + fun allYearSemester_averageFromSummary() { + `when`(preferencesRepository.gradeAverageMode).thenReturn("all_year") + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false) + `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ))) + `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ))) - val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) - .blockingGet() + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - assertEquals(2, averages.size) - assertEquals(3.1, averages.single { it.first == "Matematyka" }.second, .0) - assertEquals(3.26, averages.single { it.first == "Fizyka" }.second, .0) + assertEquals(2, items.size) + assertEquals(3.25, items.single { it.subject == "Matematyka" }.average, .0) + assertEquals(3.75, items.single { it.subject == "Fizyka" }.average, .0) } @Test fun onlyOneSemester_averageFromSummary_forceCalc() { - doReturn(true).`when`(preferencesRepository).gradeAverageForceCalc - doReturn("all_year").`when`(preferencesRepository).gradeAverageMode - doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], false) - doReturn(Single.just(listOf( - getSummary(22, "Matematyka", 3.1), - getSummary(22, "Fizyka", 3.26) - ))).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true) + `when`(preferencesRepository.gradeAverageMode).thenReturn("all_year") + `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) + `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries)) + `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ))) - val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) - .blockingGet() + val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - assertEquals(2, averages.size) - assertEquals(3.0, averages.single { it.first == "Matematyka" }.second, .0) - assertEquals(3.25, averages.single { it.first == "Fizyka" }.second, .0) + assertEquals(2, items.size) + assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0) + assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) } private fun getGrade(semesterId: Int, subject: String, value: Double, modifier: Double = 0.0): Grade { @@ -221,12 +248,12 @@ class GradeAverageProviderTest { ) } - private fun getSummary(semesterId: Int, subject: String, value: Double): GradeSummary { + private fun getSummary(semesterId: Int, subject: String, average: Double): GradeSummary { return GradeSummary( studentId = 101, semesterId = semesterId, subject = subject, - average = value, + average = average, pointsSum = "", proposedPoints = "", finalPoints = "", diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index 258dc54f..121391de 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -53,7 +53,7 @@ class LoginStudentSelectPresenterTest { fun onSelectedStudentTest() { doReturn(Single.just(listOf(1L))).`when`(studentRepository).saveStudents(listOf(testStudent)) doReturn(Completable.complete()).`when`(studentRepository).switchStudent(testStudent) - presenter.onItemSelected(LoginStudentSelectItem(testStudent, false)) + presenter.onItemSelected(testStudent, false) presenter.onSignIn() verify(loginStudentSelectView).showContent(false) verify(loginStudentSelectView).showProgress(true) @@ -64,7 +64,7 @@ class LoginStudentSelectPresenterTest { fun onSelectedStudentErrorTest() { doReturn(Single.error(testException)).`when`(studentRepository).saveStudents(listOf(testStudent)) doReturn(Completable.complete()).`when`(studentRepository).logoutStudent(testStudent) - presenter.onItemSelected(LoginStudentSelectItem(testStudent, false)) + presenter.onItemSelected(testStudent, false) presenter.onSignIn() verify(loginStudentSelectView).showContent(false) verify(loginStudentSelectView).showProgress(true) diff --git a/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt new file mode 100644 index 00000000..33a79850 --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt @@ -0,0 +1,43 @@ +package io.github.wulkanowy.utils + +import io.github.wulkanowy.getTimetableEntity +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.threeten.bp.LocalDateTime.now + +class TimetableExtensionTest { + + @Test + fun isShowTimeUntil() { + assertFalse(getTimetableEntity().isShowTimeUntil(null)) + assertFalse(getTimetableEntity(isStudentPlan = false).isShowTimeUntil(null)) + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = true).isShowTimeUntil(null)) + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().minusSeconds(1)).isShowTimeUntil(null)) + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().plusMinutes(5)).isShowTimeUntil(now().plusMinutes(5))) + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().plusMinutes(61)).isShowTimeUntil(now().minusMinutes(5))) + + assertTrue(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().plusMinutes(60)).isShowTimeUntil(now().minusMinutes(5))) + assertTrue(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().plusMinutes(60)).isShowTimeUntil(null)) + + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().minusSeconds(1)).isShowTimeUntil(null)) + } + + @Test + fun getLeft() { + assertEquals(null, getTimetableEntity(canceled = true).left) + assertEquals(null, getTimetableEntity(start = now().plusMinutes(5), end = now().plusMinutes(50)).left) + assertEquals(null, getTimetableEntity(start = now().minusMinutes(1), end = now().plusMinutes(44), isStudentPlan = false).left) + assertNotEquals(null, getTimetableEntity(start = now().minusMinutes(1), end = now().plusMinutes(44), isStudentPlan = true).left) + } + + @Test + fun isJustFinished() { + assertFalse(getTimetableEntity(end = now().minusSeconds(16)).isJustFinished) + assertTrue(getTimetableEntity(end = now().minusSeconds(14)).isJustFinished) + assertTrue(getTimetableEntity(end = now().minusSeconds(1)).isJustFinished) + assertFalse(getTimetableEntity(end = now().plusSeconds(1)).isJustFinished) + } +} diff --git a/build.gradle b/build.gradle index 658cb977..2b92bc7d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,15 +6,13 @@ buildscript { google() jcenter() maven { url "https://plugins.gradle.org/m2/" } - maven { url 'https://maven.fabric.io/public' } } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.android.tools.build:gradle:3.6.3' classpath 'com.google.gms:google-services:4.3.3' - //noinspection GradleDependency https://github.com/firebase/firebase-android-sdk/issues/1276#issuecomment-592098283 - classpath "io.fabric.tools:gradle:1.31.0" - classpath "com.github.triplet.gradle:play-publisher:2.7.3" + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.0' + classpath "com.github.triplet.gradle:play-publisher:2.7.5" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${about_libraries}" diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..2399a56b --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,14 @@ +files: + - source: /app/src/main/res/values/*.xml + ignore: + - /app/src/main/res/values/styles.xml + - /app/src/main/res/values/api_hosts.xml + - /app/src/main/res/values/api_symbols.xml + - /app/src/main/res/values/attrs.xml + - /app/src/main/res/values/colors.xml + - /app/src/main/res/values/dimens.xml + - /app/src/main/res/values/preferences_keys.xml + - /app/src/main/res/values/preferences_defaults.xml + translation: /app/src/main/res/values-%android_code%/%original_file_name% + translate_attributes: 0 + content_segmentation: 0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84a90661..6623300b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists