diff --git a/.bettercodehub.yml b/.bettercodehub.yml index d7be51695..349f7675a 100644 --- a/.bettercodehub.yml +++ b/.bettercodehub.yml @@ -1,3 +1,3 @@ -component_depth: 10 +component_depth: 8 languages: - kotlin diff --git a/.travis.yml b/.travis.yml index a24058083..514e37c0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ cache: branches: only: - develop - - 0.19.0 + - 0.17.4 android: licenses: @@ -48,15 +48,20 @@ before_script: script: - ./gradlew dependencies --stacktrace --daemon - fossa --no-ansi || true - - ./gradlew -Pcoverage testPlayDebugUnitTest --stacktrace --daemon + #- ./gradlew lintPlayRelease -x fabricGenerateResourcesPlayRelease --stacktrace --daemon + - ./gradlew -Pcoverage testPlayDebugUnitTest -x fabricGenerateResourcesPlay --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 -PenableFirebase --stacktrace; + ./gradlew publishPlayRelease -PenableCrashlytics --stacktrace; fi after_success: diff --git a/app/build.gradle b/app/build.gradle index 621e5a4fe..45d20f96d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' -apply plugin: 'com.google.firebase.crashlytics' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'io.fabric' apply plugin: 'com.github.triplet.play' apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply from: 'jacoco.gradle' @@ -17,14 +18,14 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 17 targetSdkVersion 29 - versionCode 63 - versionName "0.19.0" + versionCode 57 + versionName "0.17.4" multiDexEnabled true - resValue "string", "app_name", "Wulkanowy" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase") + fabric_api_key : System.getenv("FABRIC_API_KEY") ?: "null", + crashlytics_enabled: project.hasProperty("enableCrashlytics") ] javaCompileOptions { annotationProcessorOptions { @@ -51,17 +52,18 @@ android { buildTypes { release { + buildConfigField "boolean", "CRASHLYTICS_ENABLED", "true" minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } debug { - resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode + buildConfigField "boolean", "CRASHLYTICS_ENABLED", project.hasProperty("enableCrashlytics") ? "true" : "false" applicationIdSuffix ".dev" versionNameSuffix "-dev" testCoverageEnabled = project.hasProperty('coverage') - ext.enableCrashlytics = project.hasProperty("enableFirebase") + ext.enableCrashlytics = project.hasProperty("enableCrashlytics") } } @@ -73,14 +75,11 @@ android { } fdroid { + buildConfigField "boolean", "CRASHLYTICS_ENABLED", "false" dimension "platform" } } - buildFeatures { - viewBinding = true - } - lintOptions { disable 'HardwareIds' } @@ -104,6 +103,10 @@ android { } } +androidExtensions { + experimental = true +} + play { serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" serviceAccountCredentials = file('key.p12') @@ -114,7 +117,8 @@ play { ext { work_manager = "2.3.4" room = "2.2.5" - dagger = "2.28" + dagger = "2.27" + // don't update https://github.com/ChuckerTeam/chucker/issues/242 chucker = "3.2.0" mockk = "1.9.2" } @@ -124,14 +128,14 @@ configurations.all { } dependencies { - implementation "io.github.wulkanowy:sdk:0.19.0" + implementation "io.github.wulkanowy:sdk:0.17.4" 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-rc01" + implementation "androidx.appcompat:appcompat:1.2.0-beta01" implementation "androidx.appcompat:appcompat-resources:1.1.0" - implementation "androidx.fragment:fragment-ktx:1.2.5" + implementation "androidx.fragment:fragment-ktx:1.2.4" implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.multidex:multidex:2.0.1" @@ -142,7 +146,7 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:1.1.3" implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" implementation "com.google.android.material:material:1.1.0" - implementation "com.github.wulkanowy:material-chips-input:2.1.1" + implementation "com.github.wulkanowy:material-chips-input:2.0.1" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation "me.zhanghai.android.materialprogressbar:library:1.6.1" @@ -163,30 +167,30 @@ 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.2" + implementation "com.github.YarikSOffice:lingver:1.2.1" - implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.8" + implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.6" 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.4" + implementation "com.jakewharton.threetenabp:threetenabp:1.2.3" 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.11.0" - implementation "io.github.wulkanowy:AppKillerManager:3.0.0" - implementation 'me.xdrop:fuzzywuzzy:1.3.1' + implementation "io.coil-kt:coil:0.9.5" - playImplementation 'com.google.firebase:firebase-analytics:17.4.3' - playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.7' - playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.7" - playImplementation 'com.google.firebase:firebase-messaging:20.2.0' - playImplementation 'com.google.firebase:firebase-crashlytics:17.0.1' + 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.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" @@ -196,7 +200,7 @@ dependencies { testImplementation "junit:junit:4.13" testImplementation "io.mockk:mockk:$mockk" - testImplementation "org.threeten:threetenbp:1.4.4" + testImplementation "org.threeten:threetenbp:1.4.3" testImplementation "org.mockito:mockito-inline:3.3.3" androidTestImplementation "androidx.test:core:1.2.0" diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/25.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/25.json index 474824df6..e99a11d4a 100644 --- a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/25.json +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/25.json @@ -1741,4 +1741,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd101f5a26a024f62e6fee161e421b882')" ] } -} +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/26.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/26.json deleted file mode 100644 index 21005f9c6..000000000 --- a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/26.json +++ /dev/null @@ -1,1768 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 26, - "identityHash": "43f8777ffe95a5a2850ee981db3dbe2e", - "entities": [ - { - "tableName": "Students", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "scrapperBaseUrl", - "columnName": "scrapper_base_url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "mobileBaseUrl", - "columnName": "mobile_base_url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "loginType", - "columnName": "login_type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "loginMode", - "columnName": "login_mode", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "certificateKey", - "columnName": "certificate_key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "privateKey", - "columnName": "private_key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isParent", - "columnName": "is_parent", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "password", - "columnName": "password", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "symbol", - "columnName": "symbol", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "userLoginId", - "columnName": "user_login_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentName", - "columnName": "student_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "schoolSymbol", - "columnName": "school_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "schoolShortName", - "columnName": "school_short", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "schoolName", - "columnName": "school_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "className", - "columnName": "class_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "classId", - "columnName": "class_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isCurrent", - "columnName": "is_current", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "registrationDate", - "columnName": "registration_date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_Students_email_symbol_student_id_school_id_class_id", - "unique": true, - "columnNames": [ - "email", - "symbol", - "student_id", - "school_id", - "class_id" - ], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "Semesters", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "current", - "columnName": "is_current", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "diaryId", - "columnName": "diary_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "diaryName", - "columnName": "diary_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "schoolYear", - "columnName": "school_year", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "semesterId", - "columnName": "semester_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "semesterName", - "columnName": "semester_name", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "start", - "columnName": "start", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "end", - "columnName": "end", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "classId", - "columnName": "class_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unitId", - "columnName": "unit_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_Semesters_student_id_diary_id_semester_id", - "unique": true, - "columnNames": [ - "student_id", - "diary_id", - "semester_id" - ], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "Exams", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "diaryId", - "columnName": "diary_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "entryDate", - "columnName": "entry_date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "group", - "columnName": "group", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacher", - "columnName": "teacher", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacherSymbol", - "columnName": "teacher_symbol", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Timetable", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "diaryId", - "columnName": "diary_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "start", - "columnName": "start", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "end", - "columnName": "end", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "subjectOld", - "columnName": "subjectOld", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "group", - "columnName": "group", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "room", - "columnName": "room", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomOld", - "columnName": "roomOld", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacher", - "columnName": "teacher", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacherOld", - "columnName": "teacherOld", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "info", - "columnName": "info", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isStudentPlan", - "columnName": "student_plan", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "changes", - "columnName": "changes", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canceled", - "columnName": "canceled", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Attendance", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "diaryId", - "columnName": "diary_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "timeId", - "columnName": "time_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "presence", - "columnName": "presence", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "absence", - "columnName": "absence", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "exemption", - "columnName": "exemption", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lateness", - "columnName": "lateness", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "excused", - "columnName": "excused", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "excusable", - "columnName": "excusable", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "excuseStatus", - "columnName": "excuse_status", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "AttendanceSummary", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "diaryId", - "columnName": "diary_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subjectId", - "columnName": "subject_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "month", - "columnName": "month", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "presence", - "columnName": "presence", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "absence", - "columnName": "absence", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "absenceExcused", - "columnName": "absence_excused", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "absenceForSchoolReasons", - "columnName": "absence_for_school_reasons", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lateness", - "columnName": "lateness", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "latenessExcused", - "columnName": "lateness_excused", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "exemption", - "columnName": "exemption", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Grades", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isRead", - "columnName": "is_read", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isNotified", - "columnName": "is_notified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "semesterId", - "columnName": "semester_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entry", - "columnName": "entry", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "modifier", - "columnName": "modifier", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "comment", - "columnName": "comment", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "gradeSymbol", - "columnName": "grade_symbol", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "weight", - "columnName": "weight", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "weightValue", - "columnName": "weightValue", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "teacher", - "columnName": "teacher", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GradesSummary", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isPredictedGradeNotified", - "columnName": "is_predicted_grade_notified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isFinalGradeNotified", - "columnName": "is_final_grade_notified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "predictedGradeLastChange", - "columnName": "predicted_grade_last_change", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "finalGradeLastChange", - "columnName": "final_grade_last_change", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "semesterId", - "columnName": "semester_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "predictedGrade", - "columnName": "predicted_grade", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "finalGrade", - "columnName": "final_grade", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "proposedPoints", - "columnName": "proposed_points", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "finalPoints", - "columnName": "final_points", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "pointsSum", - "columnName": "points_sum", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "average", - "columnName": "average", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GradesStatistics", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `grade` INTEGER NOT NULL, `amount` INTEGER NOT NULL, `is_semester` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "semesterId", - "columnName": "semester_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "grade", - "columnName": "grade", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "semester", - "columnName": "is_semester", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GradesPointsStatistics", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "semesterId", - "columnName": "semester_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "others", - "columnName": "others", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "student", - "columnName": "student", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isNotified", - "columnName": "is_notified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "realId", - "columnName": "real_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "messageId", - "columnName": "message_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "sender", - "columnName": "sender_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderId", - "columnName": "sender_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipient", - "columnName": "recipient_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "folderId", - "columnName": "folder_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unread", - "columnName": "unread", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadBy", - "columnName": "unread_by", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "readBy", - "columnName": "read_by", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "removed", - "columnName": "removed", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "hasAttachments", - "columnName": "has_attachments", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "MessageAttachments", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", - "fields": [ - { - "fieldPath": "realId", - "columnName": "real_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "messageId", - "columnName": "message_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "oneDriveId", - "columnName": "one_drive_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "filename", - "columnName": "filename", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "real_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isRead", - "columnName": "is_read", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isNotified", - "columnName": "is_notified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "teacher", - "columnName": "teacher", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacherSymbol", - "columnName": "teacher_symbol", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "category", - "columnName": "category", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "categoryType", - "columnName": "category_type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isPointsShow", - "columnName": "is_points_show", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "points", - "columnName": "points", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Homework", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDone", - "columnName": "is_done", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "semesterId", - "columnName": "semester_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "entryDate", - "columnName": "entry_date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacher", - "columnName": "teacher", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacherSymbol", - "columnName": "teacher_symbol", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "attachments", - "columnName": "attachments", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Subjects", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "diaryId", - "columnName": "diary_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "realId", - "columnName": "real_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "LuckyNumbers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isNotified", - "columnName": "is_notified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "luckyNumber", - "columnName": "lucky_number", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CompletedLesson", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "diaryId", - "columnName": "diary_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "topic", - "columnName": "topic", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacher", - "columnName": "teacher", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "teacherSymbol", - "columnName": "teacher_symbol", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "substitution", - "columnName": "substitution", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "absence", - "columnName": "absence", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "resources", - "columnName": "resources", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "ReportingUnits", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "realId", - "columnName": "real_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "shortName", - "columnName": "short", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderId", - "columnName": "sender_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "senderName", - "columnName": "sender_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roles", - "columnName": "roles", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Recipients", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "realId", - "columnName": "real_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "realName", - "columnName": "real_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "loginId", - "columnName": "login_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unitId", - "columnName": "unit_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "role", - "columnName": "role", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "MobileDevices", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deviceId", - "columnName": "device_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Teachers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "classId", - "columnName": "class_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subject", - "columnName": "subject", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shortName", - "columnName": "short_name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "School", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "studentId", - "columnName": "student_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "classId", - "columnName": "class_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "contact", - "columnName": "contact", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "headmaster", - "columnName": "headmaster", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "pedagogue", - "columnName": "pedagogue", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '43f8777ffe95a5a2850ee981db3dbe2e')" - ] - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt b/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt index da4284b02..6f9406f6d 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt @@ -120,23 +120,23 @@ class Migration13Test : AbstractMigrationTest() { assertEquals(2, first.diaryId) } - getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let { semesters -> - assertTrue { semesters.single { it.second }.second } - assertEquals(1970, semesters[0].first.schoolYear) - assertEquals(of(1970, 1, 1), semesters[0].first.end) - assertEquals(of(1970, 1, 1), semesters[0].first.start) - assertFalse(semesters[0].second) - assertFalse(semesters[1].second) - assertFalse(semesters[2].second) - assertTrue(semesters[3].second) + getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let { + assertTrue { it.single { it.second }.second } + assertEquals(1970, it[0].first.schoolYear) + assertEquals(of(1970, 1, 1), it[0].first.end) + assertEquals(of(1970, 1, 1), it[0].first.start) + assertFalse(it[0].second) + assertFalse(it[1].second) + assertFalse(it[2].second) + assertTrue(it[3].second) } - getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let { semesters -> - assertTrue { semesters.single { it.second }.second } - assertFalse(semesters[0].second) - assertFalse(semesters[1].second) - assertFalse(semesters[2].second) - assertTrue(semesters[3].second) + getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let { + assertTrue { it.single { it.second }.second } + assertFalse(it[0].second) + assertFalse(it[1].second) + assertFalse(it[2].second) + assertTrue(it[3].second) } } diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/attendance/AttendanceLocalTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/attendance/AttendanceLocalTest.kt index cdfc524ad..c7ede6ae5 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/attendance/AttendanceLocalTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/attendance/AttendanceLocalTest.kt @@ -10,7 +10,6 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.of import kotlin.test.assertEquals @@ -36,18 +35,9 @@ class AttendanceLocalTest { @Test fun saveAndReadTest() { attendanceLocal.saveAttendance(listOf( - getAttendanceEntity( - of(2018, 9, 10), - SentExcuseStatus.ACCEPTED - ), - getAttendanceEntity( - of(2018, 9, 14), - SentExcuseStatus.WAITING - ), - getAttendanceEntity( - of(2018, 9, 17), - SentExcuseStatus.ACCEPTED - ) + Attendance(1, 2, 3, of(2018, 9, 10), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name), + Attendance(1, 2, 3, of(2018, 9, 14), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.WAITING.name), + Attendance(1, 2, 3, of(2018, 9, 17), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name) )) val attendance = attendanceLocal @@ -60,25 +50,4 @@ class AttendanceLocalTest { assertEquals(attendance[0].date, of(2018, 9, 10)) assertEquals(attendance[1].date, of(2018, 9, 14)) } - - private fun getAttendanceEntity( - date: LocalDate, - excuseStatus: SentExcuseStatus - ) = Attendance( - studentId = 1, - diaryId = 2, - timeId = 3, - date = date, - number = 0, - subject = "", - name = "", - presence = false, - absence = false, - exemption = false, - lateness = false, - excused = false, - deleted = false, - excusable = false, - excuseStatus = excuseStatus.name - ) } 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 eb1a55480..7233c306a 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, testDb.gradeSummaryDao) + gradeLocal = GradeLocal(testDb.gradeDao) } @After @@ -43,7 +43,7 @@ class GradeLocalTest { val semester = Semester(1, 2, "", 2019, 2, 1, now(), now(), 1, 1) val grades = gradeLocal - .getGradesDetails(semester) + .getGrades(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 cdd514772..f3c6b7a17 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,7 +11,6 @@ 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 @@ -53,7 +52,7 @@ class GradeRepositoryTest { fun initApi() { MockKAnnotations.init(this) testDb = Room.inMemoryDatabaseBuilder(getApplicationContext(), AppDatabase::class.java).build() - gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao) + gradeLocal = GradeLocal(testDb.gradeDao) gradeRemote = GradeRemote(mockSdk) every { studentMock.registrationDate } returns LocalDateTime.of(2019, 2, 27, 12, 0) @@ -76,10 +75,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().first.sortedByDescending { it.date } + .getGrades(studentMock, semesterMock, true).blockingGet().sortedByDescending { it.date } assertFalse { grades[0].isRead } assertFalse { grades[1].isRead } @@ -100,10 +99,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().first.sortedByDescending { it.date } + .getGrades(studentMock, semesterMock, true).blockingGet().sortedByDescending { it.date } assertFalse { grades[0].isRead } assertFalse { grades[1].isRead } @@ -122,12 +121,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.first.size) + assertEquals(2, grades.size) } @Test @@ -141,12 +140,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.first.size) + assertEquals(3, grades.size) } @Test @@ -157,12 +156,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.first.size) + assertEquals(3, grades.size) } @Test @@ -172,11 +171,11 @@ class GradeRepositoryTest { createGradeLocal(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") )) - every { mockSdk.getGrades(1) } returns Single.just(emptyList() to emptyList()) + every { mockSdk.getGrades(1) } returns Single.just(listOf()) val grades = GradeRepository(settings, gradeLocal, gradeRemote) .getGrades(studentMock, semesterMock, true).blockingGet() - assertEquals(0, grades.first.size) + assertEquals(0, grades.size) } } diff --git a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/luckynumber/LuckyNumberLocalTest.kt b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/luckynumber/LuckyNumberLocalTest.kt index efad0d4d5..65ef1fcbf 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/data/repositories/luckynumber/LuckyNumberLocalTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/data/repositories/luckynumber/LuckyNumberLocalTest.kt @@ -5,6 +5,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.entities.LuckyNumber +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import org.junit.After import org.junit.Before 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 75f2f0b83..fdf193a26 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,15 +8,12 @@ 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 @@ -37,17 +34,11 @@ 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 @@ -61,17 +52,10 @@ 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 } @@ -96,7 +80,7 @@ class TimetableRepositoryTest { createTimetableRemote(of(2019, 3, 5, 10, 30), 4, "", "W-F") )) - val lessons = TimetableRepository(settings, timetableLocal, timetableRemote, timetableNotificationSchedulerHelper) + val lessons = TimetableRepository(settings, timetableLocal, timetableRemote) .getTimetable(student, semesterMock, LocalDate.of(2019, 3, 5), LocalDate.of(2019, 3, 5), true) .blockingGet() @@ -142,7 +126,7 @@ class TimetableRepositoryTest { createTimetableRemote(of(2019, 12, 25, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true) )) - val lessons = TimetableRepository(settings, timetableLocal, timetableRemote, timetableNotificationSchedulerHelper) + val lessons = TimetableRepository(settings, timetableLocal, timetableRemote) .getTimetable(student, semesterMock, LocalDate.of(2019, 12, 23), LocalDate.of(2019, 12, 25), true) .blockingGet() diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 000000000..c90641ddb --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,3 @@ + + Wulkanowy DEV + diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt index d03a319a2..60dc6b56a 100644 --- a/app/src/fdroid/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt @@ -2,8 +2,11 @@ package io.github.wulkanowy.utils +import android.content.Context import timber.log.Timber +fun initCrashlytics(context: Context, appInfo: AppInfo) {} + open class TimberTreeNoOp : Timber.Tree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {} } diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt index f23645bc3..0b1274f15 100644 --- a/app/src/fdroid/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt @@ -1,18 +1,13 @@ package io.github.wulkanowy.utils -import android.app.Activity import javax.inject.Inject import javax.inject.Singleton @Singleton -@Suppress("UNUSED_PARAMETER") class FirebaseAnalyticsHelper @Inject constructor() { + @Suppress("UNUSED_PARAMETER") fun logEvent(name: String, vararg params: Pair) { // do nothing } - - fun setCurrentScreen(activity: Activity, name: String?) { - // do nothing - } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 802cf1ad2..42c754756 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,8 +18,7 @@ android:supportsRtl="false" android:theme="@style/WulkanowyTheme" android:usesCleartextTraffic="true" - tools:ignore="GoogleAppIndexingWarning,UnusedAttribute" - tools:replace="android:supportsRtl,android:allowBackup"> + tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> + android:theme="@style/WulkanowyTheme.NoActionBar" /> - - - - - - - - - + android:name="io.fabric.ApiKey" + android:value="${fabric_api_key}" /> - - - - + android:value="${crashlytics_enabled}" /> - - - - %SUBJECT% | Wulkanowy - - - -

%SUBJECT%

-
-
- %INFO% -
- -
-
-

Treść wiadomości

- %CONTENT% -
- - diff --git a/app/src/main/assets/wulkanowy-logo-black.svg b/app/src/main/assets/wulkanowy-logo-black.svg deleted file mode 100644 index 9bfbe2c02..000000000 --- a/app/src/main/assets/wulkanowy-logo-black.svg +++ /dev/null @@ -1,74 +0,0 @@ - -image/svg+xml \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt index 0e094f699..96ec7cb84 100644 --- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt +++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt @@ -10,6 +10,8 @@ import com.jakewharton.threetenabp.AndroidThreeTen import com.yariksoffice.lingver.Lingver import dagger.android.AndroidInjector import dagger.android.support.DaggerApplication +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.utils.Log import fr.bipi.tressence.file.FileLoggerTree import io.github.wulkanowy.di.DaggerAppComponent import io.github.wulkanowy.services.sync.SyncWorkerFactory @@ -19,6 +21,7 @@ import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.CrashlyticsExceptionTree import io.github.wulkanowy.utils.CrashlyticsTree import io.github.wulkanowy.utils.DebugLogTree +import io.github.wulkanowy.utils.initCrashlytics import io.reactivex.exceptions.UndeliverableException import io.reactivex.plugins.RxJavaPlugins import timber.log.Timber @@ -49,10 +52,12 @@ class WulkanowyApp : DaggerApplication(), Configuration.Provider { themeManager.applyDefaultTheme() initLogging() + initCrashlytics(this, appInfo) } private fun initLogging() { if (appInfo.isDebug) { + FlexibleAdapter.enableLogs(Log.Level.DEBUG) Timber.plant(DebugLogTree()) Timber.plant(FileLoggerTree.Builder() .withFileName("wulkanowy.%g.log") diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index 19af1b2f8..43c27c529 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -5,11 +5,11 @@ import android.content.SharedPreferences import android.content.res.AssetManager import android.content.res.Resources import androidx.preference.PreferenceManager +import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings +import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager -import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings -import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy import dagger.Module import dagger.Provides import io.github.wulkanowy.data.db.AppDatabase diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index a31b68c0d..762c52f8f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -68,7 +68,6 @@ import io.github.wulkanowy.data.db.migrations.Migration22 import io.github.wulkanowy.data.db.migrations.Migration23 import io.github.wulkanowy.data.db.migrations.Migration24 import io.github.wulkanowy.data.db.migrations.Migration25 -import io.github.wulkanowy.data.db.migrations.Migration26 import io.github.wulkanowy.data.db.migrations.Migration3 import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration5 @@ -111,7 +110,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 26 + const val VERSION_SCHEMA = 25 fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array { return arrayOf( @@ -138,8 +137,7 @@ abstract class AppDatabase : RoomDatabase() { Migration22(), Migration23(), Migration24(), - Migration25(), - Migration26() + Migration25() ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt index dd3126d4d..6e29112b2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt @@ -3,7 +3,6 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import org.threeten.bp.LocalDateTime @Entity(tableName = "GradesSummary") data class GradeSummary( @@ -37,16 +36,4 @@ data class GradeSummary( ) { @PrimaryKey(autoGenerate = true) var id: Long = 0 - - @ColumnInfo(name = "is_predicted_grade_notified") - var isPredictedGradeNotified: Boolean = true - - @ColumnInfo(name = "is_final_grade_notified") - var isFinalGradeNotified: Boolean = true - - @ColumnInfo(name = "predicted_grade_last_change") - var predictedGradeLastChange: LocalDateTime = LocalDateTime.now() - - @ColumnInfo(name = "final_grade_last_change") - var finalGradeLastChange: LocalDateTime = LocalDateTime.now() } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration26.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration26.kt deleted file mode 100644 index 7130d86d8..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration26.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.wulkanowy.data.db.migrations - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration26 : Migration(25, 26) { - - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE GradesSummary ADD COLUMN is_predicted_grade_notified INTEGER NOT NULL DEFAULT 1") - database.execSQL("ALTER TABLE GradesSummary ADD COLUMN is_final_grade_notified INTEGER NOT NULL DEFAULT 1") - database.execSQL("ALTER TABLE GradesSummary ADD COLUMN predicted_grade_last_change INTEGER NOT NULL DEFAULT 0") - database.execSQL("ALTER TABLE GradesSummary ADD COLUMN final_grade_last_change INTEGER NOT NULL DEFAULT 0") - } -} diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/AppCreator.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/AppCreator.kt new file mode 100644 index 000000000..d67aa2a7f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/AppCreator.kt @@ -0,0 +1,3 @@ +package io.github.wulkanowy.data.pojos + +class AppCreator(val displayName: String, val githubUsername: String) diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt deleted file mode 100644 index e792bde46..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.wulkanowy.data.pojos - -class Contributor(val displayName: String, val githubUsername: String) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/appcreator/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/appcreator/AppCreatorRepository.kt index 76cf698c9..fa752ed29 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/appcreator/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/appcreator/AppCreatorRepository.kt @@ -2,18 +2,18 @@ package io.github.wulkanowy.data.repositories.appcreator import android.content.res.AssetManager import com.google.gson.Gson -import io.github.wulkanowy.data.pojos.Contributor +import io.github.wulkanowy.data.pojos.AppCreator import io.reactivex.Single import javax.inject.Inject import javax.inject.Singleton @Singleton class AppCreatorRepository @Inject constructor(private val assets: AssetManager) { - fun getAppCreators(): Single> { - 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/attendance/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/attendance/AttendanceRepository.kt index 68e7c5f12..22fe7fa51 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/attendance/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/attendance/AttendanceRepository.kt @@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.uniqueSubtract import io.reactivex.Single @@ -22,19 +22,19 @@ class AttendanceRepository @Inject constructor( ) { fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean): Single> { - return local.getAttendance(semester, start.monday, end.sunday).filter { !forceRefresh } + return local.getAttendance(semester, start.monday, end.friday).filter { !forceRefresh } .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap { - if (it) remote.getAttendance(student, semester, start.monday, end.sunday) + if (it) remote.getAttendance(student, semester, start.monday, end.friday) else Single.error(UnknownHostException()) }.flatMap { newAttendance -> - local.getAttendance(semester, start.monday, end.sunday) + local.getAttendance(semester, start.monday, end.friday) .toSingle(emptyList()) .doOnSuccess { oldAttendance -> local.deleteAttendance(oldAttendance.uniqueSubtract(newAttendance)) local.saveAttendance(newAttendance.uniqueSubtract(oldAttendance)) } }.flatMap { - local.getAttendance(semester, start.monday, end.sunday) + local.getAttendance(semester, start.monday, end.friday) .toSingle(emptyList()) }).map { list -> list.filter { it.date in start..end } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/completedlessons/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/completedlessons/CompletedLessonsRepository.kt index 72cc93eb4..f31e30552 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/completedlessons/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/completedlessons/CompletedLessonsRepository.kt @@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.uniqueSubtract import io.reactivex.Single @@ -22,20 +22,20 @@ class CompletedLessonsRepository @Inject constructor( ) { fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single> { - return local.getCompletedLessons(semester, start.monday, end.sunday).filter { !forceRefresh } + return local.getCompletedLessons(semester, start.monday, end.friday).filter { !forceRefresh } .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .flatMap { - if (it) remote.getCompletedLessons(student, semester, start.monday, end.sunday) + if (it) remote.getCompletedLessons(student, semester, start.monday, end.friday) else Single.error(UnknownHostException()) }.flatMap { new -> - local.getCompletedLessons(semester, start.monday, end.sunday) + local.getCompletedLessons(semester, start.monday, end.friday) .toSingle(emptyList()) .doOnSuccess { old -> local.deleteCompleteLessons(old.uniqueSubtract(new)) local.saveCompletedLessons(new.uniqueSubtract(old)) } }.flatMap { - local.getCompletedLessons(semester, start.monday, end.sunday) + local.getCompletedLessons(semester, start.monday, end.friday) .toSingle(emptyList()) }).map { list -> list.filter { it.date in start..end } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/exam/ExamLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/exam/ExamLocal.kt index 389eb5835..0f484d323 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/exam/ExamLocal.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/exam/ExamLocal.kt @@ -13,7 +13,7 @@ class ExamLocal @Inject constructor(private val examDb: ExamDao) { fun getExams(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe> { return examDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate) - .filter { it.isNotEmpty() } + .filter { !it.isEmpty() } } fun saveExams(exams: List) { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/exam/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/exam/ExamRepository.kt index f29e4fdf8..452a15510 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/exam/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/exam/ExamRepository.kt @@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.uniqueSubtract import io.reactivex.Single @@ -22,20 +22,20 @@ class ExamRepository @Inject constructor( ) { fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single> { - return local.getExams(semester, start.monday, end.sunday).filter { !forceRefresh } + return local.getExams(semester, start.monday, end.friday).filter { !forceRefresh } .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .flatMap { - if (it) remote.getExams(student, semester, start.monday, end.sunday) + if (it) remote.getExams(student, semester, start.monday, end.friday) else Single.error(UnknownHostException()) }.flatMap { new -> - local.getExams(semester, start.monday, end.sunday) + local.getExams(semester, start.monday, end.friday) .toSingle(emptyList()) .doOnSuccess { old -> local.deleteExams(old.uniqueSubtract(new)) local.saveExams(new.uniqueSubtract(old)) } }.flatMap { - local.getExams(semester, start.monday, end.sunday) + local.getExams(semester, start.monday, end.friday) .toSingle(emptyList()) }).map { list -> list.filter { it.date in start..end } } } 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 52ab60178..4983a4740 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,19 +1,14 @@ 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, - private val gradeSummaryDb: GradeSummaryDao -) { +class GradeLocal @Inject constructor(private val gradeDb: GradeDao) { fun saveGrades(grades: List) { gradeDb.insertAll(grades) @@ -27,23 +22,7 @@ class GradeLocal @Inject constructor( gradeDb.updateAll(grades) } - fun updateGradesSummary(gradesSummary: List) { - gradeSummaryDb.updateAll(gradesSummary) - } - - fun getGradesDetails(semester: Semester): Maybe> { + fun getGrades(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 abb2f98c9..6f19095ba 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,7 +1,6 @@ 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 @@ -13,11 +12,11 @@ import javax.inject.Singleton @Singleton class GradeRemote @Inject constructor(private val sdk: Sdk) { - fun getGrades(student: Student, semester: Semester): Single, List>> { + fun getGrades(student: Student, semester: Semester): Single> { return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getGrades(semester.semesterId) - .map { (details, summary) -> - details.map { + .map { grades -> + grades.map { Grade( studentId = semester.studentId, semesterId = semester.semesterId, @@ -34,19 +33,6 @@ 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 2ba68b967..351ebc392 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,13 +3,11 @@ 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 import io.reactivex.Completable import io.reactivex.Single -import org.threeten.bp.LocalDateTime import java.net.UnknownHostException import javax.inject.Inject import javax.inject.Singleton @@ -21,79 +19,34 @@ class GradeRepository @Inject constructor( private val remote: GradeRemote ) { - 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) - .onEach { summary -> - val oldSummary = old.find { oldSummary -> oldSummary.subject == summary.subject } - summary.isPredictedGradeNotified = when { - summary.predictedGrade.isEmpty() -> true - notify && oldSummary?.predictedGrade != summary.predictedGrade -> false - else -> true - } - summary.isFinalGradeNotified = when { - summary.finalGrade.isEmpty() -> true - notify && oldSummary?.finalGrade != summary.finalGrade -> false - else -> true - } - - summary.predictedGradeLastChange = when { - oldSummary == null -> LocalDateTime.now() - summary.predictedGrade != oldSummary.predictedGrade -> LocalDateTime.now() - else -> oldSummary.predictedGradeLastChange - } - summary.finalGradeLastChange = when { - oldSummary == null -> LocalDateTime.now() - summary.finalGrade != oldSummary.finalGrade -> LocalDateTime.now() - else -> oldSummary.finalGradeLastChange - } - }) - } - } - }.flatMap { - local.getGradesDetails(semester).toSingle(emptyList()).flatMap { details -> - local.getGradesSummary(semester).toSingle(emptyList()).map { summary -> - details to summary - } - } - }) + 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 getUnreadGrades(semester: Semester): Single> { - return local.getGradesDetails(semester).map { it.filter { grade -> !grade.isRead } }.toSingle(emptyList()) + return local.getGrades(semester).map { it.filter { grade -> !grade.isRead } }.toSingle(emptyList()) } fun getNotNotifiedGrades(semester: Semester): Single> { - return local.getGradesDetails(semester).map { it.filter { grade -> !grade.isNotified } }.toSingle(emptyList()) - } - - fun getNotNotifiedPredictedGrades(semester: Semester): Single> { - return local.getGradesSummary(semester).map { it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } }.toSingle(emptyList()) - } - - fun getNotNotifiedFinalGrades(semester: Semester): Single> { - return local.getGradesSummary(semester).map { it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } }.toSingle(emptyList()) + return local.getGrades(semester).map { it.filter { grade -> !grade.isNotified } }.toSingle(emptyList()) } fun updateGrade(grade: Grade): Completable { @@ -103,8 +56,4 @@ class GradeRepository @Inject constructor( fun updateGrades(grades: List): Completable { return Completable.fromCallable { local.updateGrades(grades) } } - - fun updateGradesSummary(gradesSummary: List): Completable { - return Completable.fromCallable { local.updateGradesSummary(gradesSummary) } - } } 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 new file mode 100644 index 000000000..e74641d3a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryLocal.kt @@ -0,0 +1,24 @@ +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 new file mode 100644 index 000000000..1b09974dd --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryRemote.kt @@ -0,0 +1,35 @@ +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 new file mode 100644 index 000000000..f1d7a6c1c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/gradessummary/GradeSummaryRepository.kt @@ -0,0 +1,35 @@ +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/homework/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/homework/HomeworkRepository.kt index 7e8fd5c30..4454fd88e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/homework/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/homework/HomeworkRepository.kt @@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.uniqueSubtract import io.reactivex.Completable @@ -23,7 +23,7 @@ class HomeworkRepository @Inject constructor( ) { fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single> { - return Single.fromCallable { start.monday to end.sunday }.flatMap { (monday, friday) -> + return Single.fromCallable { start.monday to end.friday }.flatMap { (monday, friday) -> local.getHomework(semester, monday, friday).filter { !forceRefresh } .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .flatMap { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/message/MessageRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/message/MessageRemote.kt index 2df2e20a3..1808c048b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/message/MessageRemote.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/message/MessageRemote.kt @@ -75,6 +75,6 @@ class MessageRemote @Inject constructor(private val sdk: Sdk) { } fun deleteMessage(student: Student, message: Message): Single { - return sdk.init(student).deleteMessages(listOf(message.messageId to message.folderId)) + return sdk.init(student).deleteMessages(listOf(Pair(message.realId, message.folderId))) } } 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 7715cde0d..523caf6c9 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 @@ -3,7 +3,6 @@ package io.github.wulkanowy.data.repositories.preferences import android.content.Context import android.content.SharedPreferences import io.github.wulkanowy.R -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode import javax.inject.Inject import javax.inject.Singleton @@ -18,8 +17,8 @@ class PreferencesRepository @Inject constructor( val isShowPresent: Boolean get() = getBoolean(R.string.pref_key_attendance_present, R.bool.pref_default_attendance_present) - val gradeAverageMode: GradeAverageMode - get() = GradeAverageMode.getByValue(getString(R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode)) + val gradeAverageMode: String + get() = getString(R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode) val gradeAverageForceCalc: Boolean get() = getBoolean(R.string.pref_key_grade_average_force_calc, R.bool.pref_default_grade_average_force_calc) @@ -27,9 +26,6 @@ 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) @@ -56,10 +52,6 @@ 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) @@ -76,9 +68,6 @@ 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/recipient/RecipientLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/recipient/RecipientLocal.kt index ff8175440..9b1d4ac2f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/recipient/RecipientLocal.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/recipient/RecipientLocal.kt @@ -12,7 +12,7 @@ import javax.inject.Singleton class RecipientLocal @Inject constructor(private val recipientDb: RecipientDao) { fun getRecipients(student: Student, role: Int, unit: ReportingUnit): Maybe> { - return recipientDb.load(student.studentId, role, unit.realId).filter { it.isNotEmpty() } + return recipientDb.load(student.studentId, role, unit.realId).filter { !it.isEmpty() } } fun saveRecipients(recipients: List): List { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/reportingunit/ReportingUnitLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/reportingunit/ReportingUnitLocal.kt index 0631c668c..6f9eec3fc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/reportingunit/ReportingUnitLocal.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/reportingunit/ReportingUnitLocal.kt @@ -11,7 +11,7 @@ import javax.inject.Singleton class ReportingUnitLocal @Inject constructor(private val reportingUnitDb: ReportingUnitDao) { fun getReportingUnits(student: Student): Maybe> { - return reportingUnitDb.load(student.studentId).filter { it.isNotEmpty() } + return reportingUnitDb.load(student.studentId).filter { !it.isEmpty() } } fun getReportingUnit(student: Student, unitId: Int): Maybe { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/student/StudentRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/student/StudentRemote.kt index 4c0ffd820..9b77c9ad3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/student/StudentRemote.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/student/StudentRemote.kt @@ -14,7 +14,7 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) { private fun mapStudents(students: List, email: String, password: String): List { return students.map { student -> Student( - email = email.ifBlank { student.email }, + email = email, password = password, isParent = student.isParent, symbol = student.symbol, @@ -39,7 +39,7 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) { } fun getStudentsMobileApi(token: String, pin: String, symbol: String): Single> { - return sdk.getStudentsFromMobileApi(token, pin, symbol, "").map { mapStudents(it, "", "") } + return sdk.getStudentsFromMobileApi(token, pin, symbol).map { mapStudents(it, "", "") } } fun getStudentsScrapper(email: String, password: String, scrapperBaseUrl: String, symbol: String): Single> { @@ -47,6 +47,6 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) { } fun getStudentsHybrid(email: String, password: String, scrapperBaseUrl: String, symbol: String): Single> { - return sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).map { mapStudents(it, email, password) } + return sdk.getStudentsHybrid(email, password, scrapperBaseUrl, symbol).map { mapStudents(it, email, password) } } } 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 4a7a0eb24..42812b30f 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,8 +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.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.uniqueSubtract import io.reactivex.Single @@ -19,22 +18,21 @@ import javax.inject.Singleton class TimetableRepository @Inject constructor( private val settings: InternetObservingSettings, private val local: TimetableLocal, - private val remote: TimetableRemote, - private val schedulerHelper: TimetableNotificationSchedulerHelper + private val remote: TimetableRemote ) { fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single> { - return Single.fromCallable { start.monday to end.sunday }.flatMap { (monday, sunday) -> - local.getTimetable(semester, monday, sunday).filter { !forceRefresh } + return Single.fromCallable { start.monday to end.friday }.flatMap { (monday, friday) -> + local.getTimetable(semester, monday, friday).filter { !forceRefresh } .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap { - if (it) remote.getTimetable(student, semester, monday, sunday) + if (it) remote.getTimetable(student, semester, monday, friday) else Single.error(UnknownHostException()) }.flatMap { new -> - local.getTimetable(semester, monday, sunday) + local.getTimetable(semester, monday, friday) .toSingle(emptyList()) .doOnSuccess { old -> - local.deleteTimetable(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) }) - local.saveTimetable(new.uniqueSubtract(old).also { schedulerHelper.scheduleNotifications(it, student) }.map { item -> + local.deleteTimetable(old.uniqueSubtract(new)) + local.saveTimetable(new.uniqueSubtract(old).map { item -> item.also { new -> old.singleOrNull { new.start == it.start }?.let { old -> return@map new.copy( @@ -46,8 +44,8 @@ class TimetableRepository @Inject constructor( }) } }.flatMap { - local.getTimetable(semester, monday, sunday).toSingle(emptyList()) - }).map { list -> list.filter { it.date in start..end }.also { schedulerHelper.scheduleNotifications(it, student) } } + local.getTimetable(semester, monday, friday).toSingle(emptyList()) + }).map { list -> list.filter { it.date in start..end } } } } } 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 db5ff59b3..4f5683850 100644 --- a/app/src/main/java/io/github/wulkanowy/di/AppModule.kt +++ b/app/src/main/java/io/github/wulkanowy/di/AppModule.kt @@ -5,6 +5,8 @@ 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 @@ -21,6 +23,9 @@ 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 1b462964d..ba8c78d3f 100644 --- a/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt +++ b/app/src/main/java/io/github/wulkanowy/di/BindingModule.kt @@ -4,7 +4,6 @@ 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 @@ -49,7 +48,4 @@ 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 b87f0e683..7240a50bb 100644 --- a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt +++ b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt @@ -1,9 +1,7 @@ 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 @@ -17,13 +15,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 @@ -49,10 +47,6 @@ abstract class ServicesModule { @Singleton @Provides fun provideNotificationManager(context: Context) = NotificationManagerCompat.from(context) - - @Singleton - @Provides - fun provideAlarmManager(context: Context): AlarmManager = context.getSystemService()!! } @ContributesAndroidInjector @@ -70,6 +64,10 @@ 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 @@ -133,8 +131,4 @@ 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 deleted file mode 100644 index 0130f4673..000000000 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index 54b245ddb..000000000 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ /dev/null @@ -1,109 +0,0 @@ -package io.github.wulkanowy.services.alarm - -import android.app.AlarmManager -import android.app.AlarmManager.RTC_WAKEUP -import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT -import android.content.Context -import android.content.Intent -import androidx.core.app.AlarmManagerCompat -import androidx.core.app.NotificationManagerCompat -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_END -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_NEXT_ROOM -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_NEXT_TITLE -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_ROOM -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_START -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_TITLE -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_TYPE -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_ID -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_CURRENT -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_UPCOMING -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID -import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME -import io.github.wulkanowy.ui.modules.main.MainView -import io.github.wulkanowy.utils.toTimestamp -import org.threeten.bp.LocalDateTime -import org.threeten.bp.LocalDateTime.now -import timber.log.Timber -import javax.inject.Inject - -class TimetableNotificationSchedulerHelper @Inject constructor( - private val context: Context, - private val alarmManager: AlarmManager, - private val preferencesRepository: PreferencesRepository -) { - - private fun getRequestCode(time: LocalDateTime, studentId: Int) = (time.toTimestamp() * studentId).toInt() - - private fun getUpcomingLessonTime(index: Int, day: List, 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_UPDATE_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_UPDATE_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 965ed0ad9..dda2abe0c 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/DebugChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/DebugChannel.kt index a67350550..9b6f213bd 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/channels/DebugChannel.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/DebugChannel.kt @@ -22,7 +22,7 @@ class DebugChannel @Inject constructor( } override fun create() { - if (!appInfo.isDebug) return + if (appInfo.isDebug) return notificationManager.createNotificationChannel( NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_debug), IMPORTANCE_DEFAULT) .apply { 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 deleted file mode 100644 index a292c8b53..000000000 --- a/app/src/main/java/io/github/wulkanowy/services/sync/channels/UpcomingLessonsChannel.kt +++ /dev/null @@ -1,31 +0,0 @@ -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/AttendanceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt index 063b74825..e8579ddba 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.attendance.AttendanceRepository -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.reactivex.Completable import org.threeten.bp.LocalDate.now @@ -12,7 +12,7 @@ import javax.inject.Inject class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work { override fun create(student: Student, semester: Semester): Completable { - return attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true) + return attendanceRepository.getAttendance(student, semester, now().monday, now().friday, true) .ignoreElement() } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/CompletedLessonWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/CompletedLessonWork.kt index 26f79a0d5..0da597e0a 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/CompletedLessonWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/CompletedLessonWork.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.completedlessons.CompletedLessonsRepository -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.reactivex.Completable import org.threeten.bp.LocalDate.now @@ -14,7 +14,7 @@ class CompletedLessonWork @Inject constructor( ) : Work { override fun create(student: Student, semester: Semester): Completable { - return completedLessonsRepository.getCompletedLessons(student, semester, now().monday, now().sunday, true) + return completedLessonsRepository.getCompletedLessons(student, semester, now().monday, now().friday, true) .ignoreElement() } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt index 1ef97f59c..c6110bbbf 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.exam.ExamRepository -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.reactivex.Completable import org.threeten.bp.LocalDate.now @@ -12,6 +12,6 @@ import javax.inject.Inject class ExamWork @Inject constructor(private val examRepository: ExamRepository) : Work { override fun create(student: Student, semester: Semester): Completable { - return examRepository.getExams(student, semester, now().monday, now().sunday, true).ignoreElement() + return examRepository.getExams(student, semester, now().monday, now().friday, true).ignoreElement() } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeStatisticsWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeStatisticsWork.kt index c4681fb8f..327c71740 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeStatisticsWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeStatisticsWork.kt @@ -9,7 +9,7 @@ import javax.inject.Inject class GradeStatisticsWork @Inject constructor(private val gradeStatisticsRepository: GradeStatisticsRepository) : Work { override fun create(student: Student, semester: Semester): Completable { - return gradeStatisticsRepository.getGradesStatistics(student, semester, "Wszystkie", false, forceRefresh = true) + return gradeStatisticsRepository.getGradesStatistics(student, semester, "Wszystkie", false, true) .ignoreElement() } } 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 new file mode 100644 index 000000000..4c8e955d1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeSummaryWork.kt @@ -0,0 +1,14 @@ +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 fcdaad6e6..7559d2892 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 @@ -9,7 +9,6 @@ import androidx.core.app.NotificationCompat.PRIORITY_HIGH import androidx.core.app.NotificationManagerCompat import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.grade.GradeRepository @@ -31,21 +30,17 @@ class GradeWork @Inject constructor( override fun create(student: Student, semester: Semester): Completable { return gradeRepository.getGrades(student, semester, true, preferencesRepository.isNotificationsEnable) - .ignoreElement() - .concatWith(Completable.concatArray(gradeRepository.getNotNotifiedGrades(semester).flatMapCompletable { - if (it.isNotEmpty()) notifyDetails(it) + .flatMap { gradeRepository.getNotNotifiedGrades(semester) } + .flatMapCompletable { + if (it.isNotEmpty()) notify(it) gradeRepository.updateGrades(it.onEach { grade -> grade.isNotified = true }) - }, gradeRepository.getNotNotifiedPredictedGrades(semester).flatMapCompletable { - if (it.isNotEmpty()) notifyPredicted(it) - gradeRepository.updateGradesSummary(it.onEach { grade -> grade.isPredictedGradeNotified = true }) - }, gradeRepository.getNotNotifiedFinalGrades(semester).flatMapCompletable { - if (it.isNotEmpty()) notifyFinal(it) - gradeRepository.updateGradesSummary(it.onEach { grade -> grade.isFinalGradeNotified = true }) - })) + } } - private fun getNotificationBuilder(): NotificationCompat.Builder { - return NotificationCompat.Builder(context, NewGradesChannel.CHANNEL_ID) + private fun notify(grades: List) { + notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewGradesChannel.CHANNEL_ID) + .setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items, grades.size, grades.size)) + .setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items, grades.size, grades.size)) .setSmallIcon(R.drawable.ic_stat_grade) .setAutoCancel(true) .setPriority(PRIORITY_HIGH) @@ -54,12 +49,6 @@ class GradeWork @Inject constructor( .setContentIntent( PendingIntent.getActivity(context, MainView.Section.GRADE.id, MainActivity.getStartIntent(context, MainView.Section.GRADE, true), FLAG_UPDATE_CURRENT)) - } - - private fun notifyDetails(grades: List) { - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), getNotificationBuilder() - .setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items, grades.size, grades.size)) - .setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items, grades.size, grades.size)) .setStyle(NotificationCompat.InboxStyle().run { setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, grades.size, grades.size)) grades.forEach { addLine("${it.subject}: ${it.entry}") } @@ -68,30 +57,5 @@ class GradeWork @Inject constructor( .build() ) } - - private fun notifyPredicted(gradesSummary: List) { - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), getNotificationBuilder() - .setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items_predicted, gradesSummary.size, gradesSummary.size)) - .setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items_predicted, gradesSummary.size, gradesSummary.size)) - .setStyle(NotificationCompat.InboxStyle().run { - setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, gradesSummary.size, gradesSummary.size)) - gradesSummary.forEach { addLine("${it.subject}: ${it.predictedGrade}") } - this - }) - .build() - ) - } - - private fun notifyFinal(gradesSummary: List) { - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), getNotificationBuilder() - .setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items_final, gradesSummary.size, gradesSummary.size)) - .setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items_final, gradesSummary.size, gradesSummary.size)) - .setStyle(NotificationCompat.InboxStyle().run { - setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, gradesSummary.size, gradesSummary.size)) - gradesSummary.forEach { addLine("${it.subject}: ${it.finalGrade}") } - this - }) - .build() - ) - } } + diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt index 186d57b03..cf3484394 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.homework.HomeworkRepository -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.reactivex.Completable import org.threeten.bp.LocalDate.now @@ -12,6 +12,6 @@ import javax.inject.Inject class HomeworkWork @Inject constructor(private val homeworkRepository: HomeworkRepository) : Work { override fun create(student: Student, semester: Semester): Completable { - return homeworkRepository.getHomework(student, semester, now().monday, now().sunday, true).ignoreElement() + return homeworkRepository.getHomework(student, semester, now().monday, now().friday, true).ignoreElement() } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt index d79d24033..0990ed67a 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.timetable.TimetableRepository -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.monday import io.reactivex.Completable import org.threeten.bp.LocalDate.now @@ -12,7 +12,7 @@ import javax.inject.Inject class TimetableWork @Inject constructor(private val timetableRepository: TimetableRepository) : Work { override fun create(student: Student, semester: Semester): Completable { - return timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true) + return timetableRepository.getTimetable(student, semester, now().monday, now().friday, true) .ignoreElement() } } 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 f20fb22f3..ee74832fd 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,7 +11,6 @@ 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 @@ -21,13 +20,10 @@ 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, VB : ViewBinding> : - AppCompatActivity(), BaseView, HasAndroidInjector { - - protected var binding: VB by lifecycleAwareVariable() +abstract class BaseActivity> : AppCompatActivity(), BaseView, + HasAndroidInjector { @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 0279dccf4..fdc463714 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,13 +1,9 @@ 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 { - - protected var binding: VB by lifecycleAwareVariable() +abstract class BaseDialogFragment : DaggerAppCompatDialogFragment(), BaseView { override fun showError(text: String, error: Throwable) { showMessage(text) @@ -18,11 +14,11 @@ abstract class BaseDialogFragment : DaggerAppCompatDialogFragm } 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 deleted file mode 100644 index eee4625c9..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseExpandableAdapter.kt +++ /dev/null @@ -1,58 +0,0 @@ -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 83f787654..2f5878d0d 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,18 +1,12 @@ 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(@LayoutRes layoutId: Int) : DaggerFragment(layoutId), - BaseView { - - protected var binding: VB by lifecycleAwareVariable() +abstract class BaseFragment : DaggerFragment(), BaseView { protected var messageContainer: View? = null @@ -22,7 +16,7 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Dagger .setAction(R.string.all_details) { if (isAdded) showErrorDetailsDialog(error) } .show() } else { - (activity as? BaseActivity<*, *>)?.showError(text, error) + (activity as? BaseActivity<*>)?.showError(text, error) } } @@ -34,15 +28,15 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Dagger 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 896e4ff1c..ae3604bfd 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 @@ -9,18 +9,16 @@ import android.view.ViewGroup import android.widget.HorizontalScrollView import android.widget.Toast import android.widget.Toast.LENGTH_LONG -import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService import io.github.wulkanowy.R -import io.github.wulkanowy.databinding.DialogErrorBinding import io.github.wulkanowy.sdk.exception.FeatureDisabledException import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.exception.ServiceUnavailableException import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.getString -import io.github.wulkanowy.utils.openAppInMarket import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser +import kotlinx.android.synthetic.main.dialog_error.* import java.io.InterruptedIOException import java.io.PrintWriter import java.io.StringWriter @@ -28,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 @@ -54,7 +52,7 @@ class ErrorDialog : BaseDialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogErrorBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_error, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -64,43 +62,28 @@ class ErrorDialog : BaseDialogFragment() { error.printStackTrace(PrintWriter(this)) } - 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 { - openConfirmDialog { openEmailClient(stringWriter.toString()) } - } - errorDialogMessage.text = resources.getString(error) - errorDialogReport.isEnabled = when (error) { - is UnknownHostException, - is InterruptedIOException, - is SocketTimeoutException, - is ServiceUnavailableException, - is FeatureDisabledException, - is FeatureNotAvailableException -> false - else -> true - } + errorDialogContent.text = stringWriter.toString() + with(errorDialogHorizontalScroll) { + post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } } - } + errorDialogCopy.setOnClickListener { + val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString()) + activity?.getSystemService()?.setPrimaryClip(clip) - private fun openConfirmDialog(callback: () -> Unit) { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.dialog_error_check_update) - .setMessage(R.string.dialog_error_check_update_message) - .setNeutralButton(R.string.about_feedback) { _, _ -> callback() } - .setPositiveButton(R.string.dialog_error_check_update) { _, _ -> - requireContext().openAppInMarket(::showMessage) - } - .show() + 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 + } } private fun openEmailClient(content: String) { 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 deleted file mode 100644 index cefe6ed75..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/base/WidgetConfigureAdapter.kt +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 35dec3b4f..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutAdapter.kt +++ /dev/null @@ -1,72 +0,0 @@ -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 d85d01e9e..5a32ac837 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,10 +2,13 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import android.view.ViewGroup +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.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 @@ -14,19 +17,19 @@ import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.getCompatDrawable -import io.github.wulkanowy.utils.openAppInMarket import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser +import io.github.wulkanowy.utils.setOnItemClickListener +import kotlinx.android.synthetic.main.fragment_about.* import javax.inject.Inject -class AboutFragment : BaseFragment(R.layout.fragment_about), AboutView, - MainView.TitledView { +class AboutFragment : BaseFragment(), AboutView, MainView.TitledView { @Inject lateinit var presenter: AboutPresenter @Inject - lateinit var aboutAdapter: AboutAdapter + lateinit var aboutAdapter: FlexibleAdapter> @Inject lateinit var appInfo: AppInfo @@ -77,34 +80,34 @@ class AboutFragment : BaseFragment(R.layout.fragment_about 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.onClickListener = presenter::onItemSelected + aboutAdapter.setOnItemClickListener(presenter::onItemSelected) - with(binding.aboutRecycler) { - layoutManager = LinearLayoutManager(context) + with(aboutRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = aboutAdapter } } - override fun updateData(data: List>) { + override fun updateData(header: AboutScrollableHeader, items: List) { with(aboutAdapter) { - items = data - notifyDataSetChanged() + removeAllScrollableHeaders() + addScrollableHeader(header) + updateDataSet(items) } } - override fun openAppInMarket() { - context?.openAppInMarket(::showMessage) - } - override fun openLogViewer() { - (activity as? MainActivity)?.pushView(LogViewerFragment.newInstance()) + if (appInfo.isDebug) (activity as? MainActivity)?.pushView(LogViewerFragment.newInstance()) } override fun openDiscordInvite() { @@ -120,7 +123,7 @@ class AboutFragment : BaseFragment(R.layout.fragment_about chooserTitle = getString(R.string.about_feedback), email = "wulkanowyinc@gmail.com", subject = "Zgłoszenie błędu", - body = getString(R.string.about_feedback_template, + body = requireContext().getString(R.string.about_feedback_template, "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName ), onActivityNotFound = { 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 new file mode 100644 index 000000000..29f1cd8c8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutItem.kt @@ -0,0 +1,56 @@ +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 ee892adfc..7e740b32b 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,9 +1,9 @@ package io.github.wulkanowy.ui.modules.about +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.SchedulersProvider import timber.log.Timber @@ -13,7 +13,6 @@ class AboutPresenter @Inject constructor( schedulers: SchedulersProvider, errorHandler: ErrorHandler, studentRepository: StudentRepository, - private val appInfo: AppInfo, private val analytics: FirebaseAnalyticsHelper ) : BasePresenter(errorHandler, studentRepository, schedulers) { @@ -24,13 +23,13 @@ class AboutPresenter @Inject constructor( loadData() } - fun onItemSelected(name: String) { + fun onItemSelected(item: AbstractFlexibleItem<*>) { + if (item !is AboutItem) return view?.run { - when (name) { + when (item.title) { versionRes?.first -> { Timber.i("Opening log viewer") - if (appInfo.isDebug) openLogViewer() - else openAppInMarket() + openLogViewer() analytics.logEvent("about_open", "name" to "log_viewer") } feedbackRes?.first -> { @@ -74,16 +73,15 @@ class AboutPresenter @Inject constructor( private fun loadData() { view?.run { - updateData(listOfNotNull( - versionRes, - creatorsRes, - feedbackRes, - faqRes, - discordRes, - homepageRes, - licensesRes, - privacyRes - )) + 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) })) } } } 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 new file mode 100644 index 000000000..07bb41249 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutScrollableHeader.kt @@ -0,0 +1,41 @@ +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 4c4b002f9..4bc0c3fe0 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,9 +23,7 @@ interface AboutView : BaseView { fun initView() - fun updateData(data: List>) - - fun openAppInMarket() + fun updateData(header: AboutScrollableHeader, items: 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 deleted file mode 100644 index 215cd27db..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorAdapter.kt +++ /dev/null @@ -1,41 +0,0 @@ -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 42eebd347..c181c3d38 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,27 +1,30 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +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 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(R.layout.fragment_contributor), - ContributorView, MainView.TitledView { +class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView { @Inject lateinit var presenter: ContributorPresenter @Inject - lateinit var creatorsAdapter: ContributorAdapter + lateinit var creatorsAdapter: FlexibleAdapter> override val titleStringId get() = R.string.contributors_title @@ -29,27 +32,29 @@ class ContributorFragment : BaseFragment(R.layout.fr fun newInstance() = ContributorFragment() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentContributorBinding.bind(view) + 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) presenter.onAttachView(this) } override fun initView() { - with(binding.creatorRecycler) { - layoutManager = LinearLayoutManager(context) + with(creatorRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = creatorsAdapter - addItemDecoration(DividerItemDecoration(context)) + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider() + .withDrawDividerOnLastItem(false)) } - creatorsAdapter.onClickListener = presenter::onItemSelected - binding.creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() } + creatorsAdapter.setOnItemClickListener(presenter::onItemSelected) + creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() } } - override fun updateData(data: List) { - with(creatorsAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + creatorsAdapter.updateDataSet(data) } override fun openUserGithubPage(username: String) { @@ -61,7 +66,7 @@ class ContributorFragment : BaseFragment(R.layout.fr } override fun showProgress(show: Boolean) { - binding.creatorProgress.visibility = if (show) VISIBLE else GONE + 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 new file mode 100644 index 000000000..844b5bd8d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorItem.kt @@ -0,0 +1,51 @@ +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 416a59ce5..721b25007 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 io.github.wulkanowy.data.pojos.Contributor +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.repositories.appcreator.AppCreatorRepository import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter @@ -21,8 +21,9 @@ class ContributorPresenter @Inject constructor( loadData() } - fun onItemSelected(contributor: Contributor) { - view?.openUserGithubPage(contributor.githubUsername) + fun onItemSelected(item: AbstractFlexibleItem<*>) { + if (item !is ContributorItem) return + view?.openUserGithubPage(item.creator.githubUsername) } fun onSeeMoreClick() { @@ -31,6 +32,7 @@ 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 8007e4e3f..18ec3a8ec 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,13 +1,12 @@ 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 deleted file mode 100644 index 07025c09f..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseAdapter.kt +++ /dev/null @@ -1,34 +0,0 @@ -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 f6c3b5698..2681680b1 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,29 +1,33 @@ 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(R.layout.fragment_license), - LicenseView, MainView.TitledView { +class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView { @Inject lateinit var presenter: LicensePresenter @Inject - lateinit var licenseAdapter: LicenseAdapter + lateinit var licenseAdapter: FlexibleAdapter> @Inject lateinit var libs: Lazy @@ -39,26 +43,25 @@ class LicenseFragment : BaseFragment(R.layout.fragment_l fun newInstance() = LicenseFragment() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentLicenseBinding.bind(view) + 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) presenter.onAttachView(this) } override fun initView() { - licenseAdapter.onClickListener = presenter::onItemSelected - - with(binding.licenseRecycler) { - layoutManager = LinearLayoutManager(context) + with(licenseRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = licenseAdapter } + licenseAdapter.setOnItemClickListener(presenter::onItemSelected) } - override fun updateData(data: List) { - with(licenseAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + licenseAdapter.updateDataSet(data) } override fun openLicense(licenseHtml: String) { @@ -73,7 +76,7 @@ class LicenseFragment : BaseFragment(R.layout.fragment_l } override fun showProgress(show: Boolean) { - binding.licenseProgress.visibility = if (show) VISIBLE else GONE + 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 new file mode 100644 index 000000000..8dcb89224 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseItem.kt @@ -0,0 +1,44 @@ +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 d0f6d69e0..dc48b098b 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 com.mikepenz.aboutlibraries.entity.Library +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 @@ -20,12 +20,14 @@ class LicensePresenter @Inject constructor( loadData() } - fun onItemSelected(library: Library) { - view?.run { library.license?.licenseDescription?.let { openLicense(it) } } + fun onItemSelected(item: AbstractFlexibleItem<*>) { + if (item !is LicenseItem) return + view?.run { item.library.license?.licenseDescription?.let { openLicense(it) } } } private fun loadData() { - disposable.add(Single.fromCallable { view?.appLibraries.orEmpty() } + disposable.add(Single.fromCallable { view?.appLibraries } + .map { it.map { library -> LicenseItem(library) } } .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 0680dbb73..3939d3e80 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 08f91aff1..0b7b05c78 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,22 +8,23 @@ 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(R.layout.fragment_logviewer), - LogViewerView, MainView.TitledView { +class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView { @Inject lateinit var presenter: LogViewerPresenter @@ -42,10 +43,13 @@ class LogViewerFragment : BaseFragment(R.layout.fragme setHasOptionsMenu(true) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentLogviewerBinding.bind(view) - messageContainer = binding.logViewerRecycler + 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 presenter.onAttachView(this) } @@ -59,18 +63,18 @@ class LogViewerFragment : BaseFragment(R.layout.fragme } override fun initView() { - with(binding.logViewerRecycler) { + with(logViewerRecycler) { layoutManager = LinearLayoutManager(context) adapter = logAdapter } - binding.logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() } + logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() } } override fun setLines(lines: List) { logAdapter.lines = lines logAdapter.notifyDataSetChanged() - binding.logViewerRecycler.scrollToPosition(lines.size - 1) + logViewerRecycler.scrollToPosition(lines.size - 1) } override fun shareLogs(files: List) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/logviewer/LogViewerPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/logviewer/LogViewerPresenter.kt index 33eb1122d..4485cb3eb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/logviewer/LogViewerPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/logviewer/LogViewerPresenter.kt @@ -25,9 +25,9 @@ class LogViewerPresenter @Inject constructor( disposable.add(loggerRepository.getLogFiles() .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) - .subscribe({ files -> - Timber.i("Loading logs files result: ${files.joinToString { it.name }}") - view?.shareLogs(files) + .subscribe({ + Timber.i("Loading logs files result: ${it.joinToString { it.name }}") + view?.shareLogs(it) }, { Timber.i("Loading logs files result: An exception occurred") errorHandler.dispatch(it) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/Account.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/Account.kt deleted file mode 100644 index dbcb499e8..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/Account.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.wulkanowy.ui.modules.account - -data class Account(val email: String, val isParent: Boolean) 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 deleted file mode 100644 index 27915a710..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountAdapter.kt +++ /dev/null @@ -1,88 +0,0 @@ -package io.github.wulkanowy.ui.modules.account - -import android.annotation.SuppressLint -import android.graphics.PorterDuff -import android.view.LayoutInflater -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.databinding.HeaderAccountBinding -import io.github.wulkanowy.databinding.ItemAccountBinding -import io.github.wulkanowy.sdk.Sdk -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 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) { - AccountItem.ViewType.HEADER.id -> HeaderViewHolder(HeaderAccountBinding.inflate(inflater, parent, false)) - AccountItem.ViewType.ITEM.id -> ItemViewHolder(ItemAccountBinding.inflate(inflater, parent, false)) - else -> throw IllegalStateException() - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is HeaderViewHolder -> bindHeaderViewHolder(holder.binding, items[position].value as Account) - is ItemViewHolder -> bindItemViewHolder(holder.binding, items[position].value as Student) - } - } - - private fun bindHeaderViewHolder(binding: HeaderAccountBinding, account: Account) { - with(binding) { - accountHeaderEmail.text = account.email - accountHeaderType.setText(if (account.isParent) R.string.account_type_parent else R.string.account_type_student) - } - } - - @SuppressLint("SetTextI18n") - private fun bindItemViewHolder(binding: ItemAccountBinding, student: Student) { - with(binding) { - accountItemName.text = "${student.studentName} ${student.className}" - accountItemSchool.text = student.schoolName - with(accountItemLoginMode) { - visibility = when (Sdk.Mode.valueOf(student.loginMode)) { - Sdk.Mode.API -> { - setText(R.string.account_login_mobile_api) - VISIBLE - } - Sdk.Mode.HYBRID -> { - setText(R.string.account_login_hybrid) - VISIBLE - } - Sdk.Mode.SCRAPPER -> { - GONE - } - } - } - - with(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 HeaderViewHolder(val binding: HeaderAccountBinding) : - RecyclerView.ViewHolder(binding.root) - - 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 320613745..cfff31c98 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,20 +7,23 @@ import android.view.ViewGroup import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog -import androidx.recyclerview.widget.LinearLayoutManager +import 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.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: AccountAdapter + lateinit var accountAdapter: FlexibleAdapter> companion object { fun newInstance() = AccountDialog() @@ -32,7 +35,7 @@ class AccountDialog : BaseDialogFragment(), AccountView { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogAccountBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_account, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -41,23 +44,18 @@ class AccountDialog : BaseDialogFragment(), AccountView { } override fun initView() { - accountAdapter.onClickListener = presenter::onItemSelected + accountAdapter.setOnItemClickListener { presenter.onItemSelected(it) } - with(binding) { - accountDialogAdd.setOnClickListener { presenter.onAddSelected() } - accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() } - accountDialogRecycler.apply { - layoutManager = LinearLayoutManager(context) - adapter = accountAdapter - } + accountDialogAdd.setOnClickListener { presenter.onAddSelected() } + accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() } + accountDialogRecycler.apply { + layoutManager = SmoothScrollLinearLayoutManager(context) + adapter = accountAdapter } } - override fun updateData(data: List>) { - with(accountAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + accountAdapter.updateDataSet(data) } 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 index 05a4a69ce..d3a3ee6a3 100644 --- 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 @@ -1,9 +1,59 @@ package io.github.wulkanowy.ui.modules.account -data class AccountItem(val value: T, val viewType: ViewType) { +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.* - enum class ViewType(val id: Int) { - HEADER(1), - ITEM(2) +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 b5fbcdb63..e9b4b81ee 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 io.github.wulkanowy.data.db.entities.Student +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter @@ -63,40 +63,34 @@ class AccountPresenter @Inject constructor( })) } - 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 createAccountItems(items: List): List> { - return items.groupBy { Account(it.email, it.isParent) }.map { (account, students) -> - listOf(AccountItem(account, AccountItem.ViewType.HEADER)) + students.map { student -> - AccountItem(student, AccountItem.ViewType.ITEM) + 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) + })) } - }.flatten() + } } 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) - .map { createAccountItems(it) } .subscribe({ Timber.i("Loading account result: Success") view?.updateData(it) @@ -106,3 +100,4 @@ 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 a1f8086cd..ede5023ba 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 @@ -6,7 +6,7 @@ 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 a63d5045a..75f998404 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,80 +1,12 @@ package io.github.wulkanowy.ui.modules.attendance -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 eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible 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 @Inject constructor() : - RecyclerView.Adapter() { - - var items = emptyList() +class AttendanceAdapter> : FlexibleAdapter(null, null, true) { 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 97b76e812..611dd999e 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,15 +5,13 @@ 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 { @@ -35,18 +33,16 @@ class AttendanceDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_attendance, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - with(binding) { - attendanceDialogSubject.text = attendance.subject - attendanceDialogDescription.text = attendance.name - attendanceDialogDate.text = attendance.date.toFormattedString() - attendanceDialogNumber.text = attendance.number.toString() - attendanceDialogClose.setOnClickListener { dismiss() } - } + 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 6599243dd..9969b1c78 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,32 +10,35 @@ 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(R.layout.fragment_attendance), AttendanceView, MainView.MainChildView, +class AttendanceFragment : BaseFragment(), 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) @@ -51,7 +54,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag override val titleStringId get() = R.string.attendance_title - override val isViewEmpty get() = attendanceAdapter.items.isEmpty() + override val isViewEmpty get() = attendanceAdapter.isEmpty override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize @@ -88,38 +91,39 @@ class AttendanceFragment : BaseFragment(R.layout.frag setHasOptionsMenu(true) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentAttendanceBinding.bind(view) - messageContainer = binding.attendanceRecycler + 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 presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - with(attendanceAdapter) { - onClickListener = presenter::onAttendanceItemSelected - onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect - } + attendanceAdapter.setOnItemClickListener(presenter::onAttendanceItemSelected) + attendanceAdapter.onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect - with(binding.attendanceRecycler) { - layoutManager = LinearLayoutManager(context) + with(attendanceRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = attendanceAdapter - addItemDecoration(DividerItemDecoration(context)) + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider() + .withDrawDividerOnLastItem(false)) } - with(binding) { - attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - attendanceErrorRetry.setOnClickListener { presenter.onRetry() } - attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } + attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + attendanceErrorRetry.setOnClickListener { presenter.onRetry() } + attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } - attendancePreviousButton.setOnClickListener { presenter.onPreviousDay() } - attendanceNavDate.setOnClickListener { presenter.onPickDate() } - attendanceNextButton.setOnClickListener { presenter.onNextDay() } + attendancePreviousButton.setOnClickListener { presenter.onPreviousDay() } + attendanceNavDate.setOnClickListener { presenter.onPickDate() } + attendanceNextButton.setOnClickListener { presenter.onNextDay() } - attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } + attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } - attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) - } + attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -131,26 +135,20 @@ class AttendanceFragment : BaseFragment(R.layout.frag else false } - override fun updateData(data: List) { - with(attendanceAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + attendanceAdapter.updateDataSet(data, true) } override fun updateNavigationDay(date: String) { - binding.attendanceNavDate.text = date + attendanceNavDate.text = date } override fun clearData() { - with(attendanceAdapter) { - items = emptyList() - notifyDataSetChanged() - } + attendanceAdapter.clear() } override fun resetView() { - binding.attendanceRecycler.smoothScrollToPosition(0) + attendanceRecycler.smoothScrollToPosition(0) } override fun onFragmentReselected() { @@ -166,43 +164,43 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showEmpty(show: Boolean) { - binding.attendanceEmpty.visibility = if (show) VISIBLE else GONE + attendanceEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.attendanceError.visibility = if (show) VISIBLE else GONE + attendanceError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.attendanceErrorMessage.text = message + attendanceErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.attendanceProgress.visibility = if (show) VISIBLE else GONE + attendanceProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.attendanceSwipe.isEnabled = enable + attendanceSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding. attendanceRecycler.visibility = if (show) VISIBLE else GONE + attendanceRecycler.visibility = if (show) VISIBLE else GONE } override fun hideRefresh() { - binding.attendanceSwipe.isRefreshing = false + attendanceSwipe.isRefreshing = false } override fun showPreButton(show: Boolean) { - binding.attendancePreviousButton.visibility = if (show) VISIBLE else INVISIBLE + attendancePreviousButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showNextButton(show: Boolean) { - binding. attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE + attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showExcuseButton(show: Boolean) { - binding.attendanceExcuseButton.visibility = if (show) VISIBLE else GONE + attendanceExcuseButton.visibility = if (show) VISIBLE else GONE } override fun showAttendanceDialog(lesson: Attendance) { @@ -225,15 +223,14 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showExcuseDialog() { - val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context)) AlertDialog.Builder(requireContext()) .setTitle(R.string.attendance_excuse_title) - .setView(dialogBinding.root) + .setView(R.layout.dialog_excuse) .setNegativeButton(android.R.string.cancel) { _, _ -> } .create() .apply { setButton(BUTTON_POSITIVE, getString(R.string.attendance_excuse_dialog_submit)) { _, _ -> - presenter.onExcuseDialogSubmit(dialogBinding.excuseReason.text?.toString().orEmpty()) + presenter.onExcuseDialogSubmit(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 new file mode 100644 index 000000000..7355aec2e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceItem.kt @@ -0,0 +1,97 @@ +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 new file mode 100644 index 000000000..eb35fea1b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceModule.kt @@ -0,0 +1,12 @@ +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 f58d0617f..7fc044744 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,6 +1,7 @@ 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 @@ -20,6 +21,7 @@ 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 AttendancePresenter @Inject constructor( @@ -109,11 +111,11 @@ class AttendancePresenter @Inject constructor( view?.finishActionMode() } - fun onAttendanceItemSelected(attendance: Attendance) { + fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) { view?.apply { - if (!excuseActionMode) { - Timber.i("Select attendance item ${attendance.id}") - showAttendanceDialog(attendance) + if (item is AttendanceItem && !excuseActionMode) { + Timber.i("Select attendance item ${item.attendance.id}") + showAttendanceDialog(item.attendance) } } } @@ -195,7 +197,9 @@ class AttendancePresenter @Inject constructor( if (prefRepository.isShowPresent) list else list.filter { !it.presence } } - .map { items -> items.sortedBy { it.number } } + .delay(200, MILLISECONDS) + .map { items -> items.map { AttendanceItem(it) } } + .map { items -> items.sortedBy { it.attendance.number } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -212,14 +216,9 @@ class AttendancePresenter @Inject constructor( showEmpty(it.isEmpty()) showErrorView(false) showContent(it.isNotEmpty()) - showExcuseButton(it.any { item -> item.excusable }) + showExcuseButton(it.any { item -> item.attendance.excusable }) } - analytics.logEvent( - "load_data", - "type" to "attendance", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_attendance", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading attendance result: An exception occurred") errorHandler.dispatch(it) @@ -237,6 +236,7 @@ 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 484070a2e..03e95053f 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 deleted file mode 100644 index 236c3da16..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryAdapter.kt +++ /dev/null @@ -1,101 +0,0 @@ -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 1b9fe08ef..8e30ea8b7 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,31 +1,32 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.R -import io.github.wulkanowy.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(R.layout.fragment_attendance_summary), - AttendanceSummaryView, MainView.TitledView { +class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainView.TitledView { @Inject lateinit var presenter: AttendanceSummaryPresenter @Inject - lateinit var attendanceSummaryAdapter: AttendanceSummaryAdapter + lateinit var attendanceSummaryAdapter: FlexibleAdapter> private lateinit var subjectsAdapter: ArrayAdapter @@ -35,38 +36,41 @@ class AttendanceSummaryFragment : 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.items.isEmpty() + override val isViewEmpty get() = attendanceSummaryAdapter.isEmpty - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentAttendanceSummaryBinding.bind(view) - messageContainer = binding.attendanceSummaryRecycler + 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 presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SUBJECT_KEY)) } override fun initView() { - with(binding.attendanceSummaryRecycler) { - layoutManager = LinearLayoutManager(context) + with(attendanceSummaryRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = attendanceSummaryAdapter } - with(binding) { - attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) - attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() } - attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } - } + 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(binding.attendanceSummarySubjects) { + with(attendanceSummarySubjects) { adapter = subjectsAdapter setOnItemSelectedListener { presenter.onSubjectSelected(it?.text?.toString()) } } - binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) } override fun updateSubjects(data: ArrayList) { @@ -77,50 +81,48 @@ class AttendanceSummaryFragment : } } - override fun updateDataSet(data: List) { + override fun updateDataSet(data: List, header: AttendanceSummaryScrollableHeader) { with(attendanceSummaryAdapter) { - items = data - notifyDataSetChanged() + updateDataSet(data, true) + removeAllScrollableHeaders() + addScrollableHeader(header) } } override fun clearView() { - with(attendanceSummaryAdapter) { - items = emptyList() - notifyDataSetChanged() - } + attendanceSummaryAdapter.clear() } override fun showEmpty(show: Boolean) { - binding.attendanceSummaryEmpty.visibility = if (show) VISIBLE else GONE + attendanceSummaryEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.attendanceSummaryError.visibility = if (show) VISIBLE else GONE + attendanceSummaryError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.attendanceSummaryErrorMessage.text = message + attendanceSummaryErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.attendanceSummaryProgress.visibility = if (show) VISIBLE else GONE + attendanceSummaryProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.attendanceSummarySwipe.isEnabled = enable + attendanceSummarySwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.attendanceSummaryRecycler.visibility = if (show) VISIBLE else GONE + attendanceSummaryRecycler.visibility = if (show) VISIBLE else GONE } override fun showSubjects(show: Boolean) { - binding.attendanceSummarySubjectsContainer.visibility = if (show) VISIBLE else INVISIBLE + attendanceSummarySubjectsContainer.visibility = if (show) VISIBLE else INVISIBLE } override fun hideRefresh() { - binding.attendanceSummarySwipe.isRefreshing = false + attendanceSummarySwipe.isRefreshing = false } override fun onSaveInstanceState(outState: Bundle) { @@ -129,7 +131,7 @@ class AttendanceSummaryFragment : } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } } 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 new file mode 100644 index 000000000..265d6ce44 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryItem.kt @@ -0,0 +1,80 @@ +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 33e18c2e7..8fc5b6e4a 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,5 +1,6 @@ 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 @@ -9,8 +10,13 @@ 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( @@ -82,7 +88,8 @@ class AttendanceSummaryPresenter @Inject constructor( attendanceSummaryRepository.getAttendanceSummary(student, it, subjectId, forceRefresh) } } - .map { items -> items.sortedByDescending { if (it.month.value <= Month.JUNE.value) it.month.value + 12 else it.month.value } } + .map { createAttendanceSummaryItems(it) to AttendanceSummaryScrollableHeader(formatPercentage(it.calculatePercentage())) } + .delay(200, MILLISECONDS) .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -95,17 +102,11 @@ class AttendanceSummaryPresenter @Inject constructor( .subscribe({ Timber.i("Loading attendance summary result: Success") view?.apply { - showEmpty(it.isEmpty()) - showContent(it.isNotEmpty()) - updateDataSet(it) + showEmpty(it.first.isEmpty()) + showContent(it.first.isNotEmpty()) + updateDataSet(it.first, it.second) } - analytics.logEvent( - "load_data", - "type" to "attendance_summary", - "items" to it.size, - "force_refresh" to forceRefresh, - "item_id" to subjectId - ) + analytics.logEvent("load_attendance_summary", "items" to it.first.size, "force_refresh" to forceRefresh, "item_id" to subjectId) }) { Timber.i("Loading attendance summary result: An exception occurred") errorHandler.dispatch(it) @@ -149,4 +150,42 @@ 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 new file mode 100644 index 000000000..c258f71d2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryScrollableHeader.kt @@ -0,0 +1,46 @@ +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 dd4053c72..8bd5332d6 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,10 +1,11 @@ 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() @@ -23,7 +24,7 @@ interface AttendanceSummaryView : BaseView { fun setErrorDetails(message: String) - fun updateDataSet(data: List) + fun updateDataSet(data: List, header: AttendanceSummaryScrollableHeader) 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 deleted file mode 100644 index 85061997c..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamAdapter.kt +++ /dev/null @@ -1,65 +0,0 @@ -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 c1a6f1f0d..ed5092c96 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,15 +5,13 @@ 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 { @@ -35,20 +33,18 @@ class ExamDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogExamBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_exam, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - with(binding) { - examDialogSubjectValue.text = exam.subject - examDialogTypeValue.text = exam.type - examDialogTeacherValue.text = exam.teacher - examDialogDateValue.text = exam.entryDate.toFormattedString() - examDialogDescriptionValue.text = exam.description + 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 eeab30c15..b880f4650 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,29 +1,33 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +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 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(R.layout.fragment_exam), ExamView, - MainView.MainChildView, MainView.TitledView { +class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.TitledView { @Inject lateinit var presenter: ExamPresenter @Inject - lateinit var examAdapter: ExamAdapter + lateinit var examAdapter: FlexibleAdapter> companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" @@ -33,60 +37,57 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), override val titleStringId get() = R.string.exam_title - override val isViewEmpty get() = examAdapter.items.isEmpty() + override val isViewEmpty get() = examAdapter.isEmpty - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentExamBinding.bind(view) - messageContainer = binding.examRecycler + 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 presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - examAdapter.onClickListener = presenter::onExamItemSelected + examAdapter.setOnItemClickListener(presenter::onExamItemSelected) - with(binding.examRecycler) { - layoutManager = LinearLayoutManager(context) + with(examRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = examAdapter - addItemDecoration(DividerItemDecoration(context)) + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider(R.layout.item_exam) + .withDrawDividerOnLastItem(false)) } - with(binding) { - examSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - examErrorRetry.setOnClickListener { presenter.onRetry() } - examErrorDetails.setOnClickListener { presenter.onDetailsClick() } + 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() { - binding.examSwipe.isRefreshing = false + examSwipe.isRefreshing = false } - override fun updateData(data: List>) { - with(examAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + examAdapter.updateDataSet(data, true) } override fun updateNavigationWeek(date: String) { - binding.examNavDate.text = date + examNavDate.text = date } override fun clearData() { - with(examAdapter) { - items = emptyList() - notifyDataSetChanged() - } + examAdapter.clear() } override fun resetView() { - binding.examRecycler.scrollToPosition(0) + examRecycler.scrollToPosition(0) } override fun onFragmentReselected() { @@ -94,35 +95,35 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), } override fun showEmpty(show: Boolean) { - binding.examEmpty.visibility = if (show) VISIBLE else GONE + examEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.examError.visibility = if (show) VISIBLE else GONE + examError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.examErrorMessage.text = message + examErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.examProgress.visibility = if (show) VISIBLE else GONE + examProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.examSwipe.isEnabled = enable + examSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.examRecycler.visibility = if (show) VISIBLE else GONE + examRecycler.visibility = if (show) VISIBLE else GONE } override fun showPreButton(show: Boolean) { - binding.examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE + examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showNextButton(show: Boolean) { - binding.examNextButton.visibility = if (show) VISIBLE else INVISIBLE + 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 new file mode 100644 index 000000000..0a5b862c3 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamHeader.kt @@ -0,0 +1,52 @@ +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 579e37203..8971b4df3 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,9 +1,50 @@ package io.github.wulkanowy.ui.modules.exam -data class ExamItem(val value: T, val viewType: ViewType) { +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.* - enum class ViewType(val id: Int) { - HEADER(1), - ITEM(2) +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 } } 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 1140cb020..aac9bc4bb 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,5 +1,6 @@ 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 @@ -8,7 +9,7 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.SchedulersProvider -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.monday @@ -18,6 +19,7 @@ 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( @@ -73,9 +75,11 @@ class ExamPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - fun onExamItemSelected(exam: Exam) { - Timber.i("Select exam item ${exam.id}") - view?.showExamDialog(exam) + fun onExamItemSelected(item: AbstractFlexibleItem<*>?) { + if (item is ExamItem) { + Timber.i("Select exam item ${item.exam.id}") + view?.showExamDialog(item.exam) + } } fun onViewReselected() { @@ -110,9 +114,11 @@ class ExamPresenter @Inject constructor( add(studentRepository.getCurrentStudent() .flatMap { student -> semesterRepository.getCurrentSemester(student).flatMap { semester -> - examRepository.getExams(student, semester, currentDate.monday, currentDate.sunday, forceRefresh) + 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) @@ -131,12 +137,7 @@ class ExamPresenter @Inject constructor( showErrorView(false) showContent(it.isNotEmpty()) } - analytics.logEvent( - "load_data", - "type" to "exam", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_exam", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading exam result: An exception occurred") errorHandler.dispatch(it) @@ -155,12 +156,12 @@ class ExamPresenter @Inject constructor( } } - 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) + private fun createExamItems(items: Map>): List { + return items.flatMap { + ExamHeader(it.key).let { header -> + it.value.reversed().map { item -> ExamItem(header, item) } } - }.flatten() + } } private fun reloadView() { @@ -181,7 +182,7 @@ class ExamPresenter @Inject constructor( showPreButton(!currentDate.minusDays(7).isHolidays) showNextButton(!currentDate.plusDays(7).isHolidays) updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " + - currentDate.sunday.toFormattedString("dd.MM")) + currentDate.friday.toFormattedString("dd.MM")) } } } 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 00429bae6..5f4a74306 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/GradeAverageMode.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageMode.kt deleted file mode 100644 index 1960c3df7..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageMode.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.wulkanowy.ui.modules.grade - -enum class GradeAverageMode(val value: String) { - ALL_YEAR("all_year"), - ONE_SEMESTER("only_one_semester"), - BOTH_SEMESTERS("both_semesters"); - - companion object { - fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE_SEMESTER - } -} 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 af1699323..3bb084d3a 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 @@ -1,125 +1,73 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.db.entities.Grade -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.grade.GradeRepository +import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository -import io.github.wulkanowy.data.repositories.semester.SemesterRepository import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.changeModifier +import io.reactivex.Maybe import io.reactivex.Single import javax.inject.Inject class GradeAverageProvider @Inject constructor( - private val semesterRepository: SemesterRepository, + private val preferencesRepository: PreferencesRepository, private val gradeRepository: GradeRepository, - private val preferencesRepository: PreferencesRepository + private val gradeSummaryRepository: GradeSummaryRepository ) { - private val plusModifier get() = preferencesRepository.gradePlusModifier + private val plusModifier = preferencesRepository.gradePlusModifier - private val minusModifier get() = preferencesRepository.gradeMinusModifier + private val minusModifier = preferencesRepository.gradeMinusModifier - fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean = false): Single> { - return semesterRepository.getSemesters(student).flatMap { semesters -> - when (preferencesRepository.gradeAverageMode) { - ONE_SEMESTER -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh) - BOTH_SEMESTERS -> calculateBothSemestersAverage(student, semesters, semesterId, forceRefresh) - ALL_YEAR -> calculateAllYearAverage(student, semesters, semesterId, forceRefresh) - } + 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} ") } } - private fun calculateBothSemestersAverage(student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean): Single> { + private fun getAllYearAverage(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 getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh).flatMap { selectedDetails -> - val isAnyAverage = selectedDetails.any { it.average != .0 } - - if (selectedSemester != firstSemester) { - getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails -> - selectedDetails.map { selected -> - val second = secondDetails.singleOrNull { it.subject == selected.subject } - selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) { - val selectedGrades = selected.grades.updateModifiers(student).calcAverage() - (selectedGrades + (second?.grades?.updateModifiers(student)?.calcAverage() ?: selectedGrades)) / 2 - } else (selected.average + (second?.average ?: selected.average)) / 2) + 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 } } - } - } else Single.just(selectedDetails) - } + }.map { grades -> + grades.map { if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier) else it } + .groupBy { it.subject } + .map { Triple(it.key, it.value.calcAverage(), "") } + }) } - private fun calculateAllYearAverage(student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean): Single> { + private fun getOnlyOneSemesterAverage(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 getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh).flatMap { selectedDetails -> - val isAnyAverage = selectedDetails.any { it.average != .0 } - - if (selectedSemester != firstSemester) { - getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails -> - selectedDetails.map { selected -> - val second = secondDetails.singleOrNull { it.subject == selected.subject } - selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) { - (selected.grades.updateModifiers(student) + second?.grades?.updateModifiers(student).orEmpty()).calcAverage() - } else selected.average) - } - } - } else Single.just(selectedDetails) - } + 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 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 } - - summaries.emulateEmptySummaries(student, semester, allGrades.toList(), isAnyAverage).map { summary -> - val grades = allGrades[summary.subject].orEmpty() - GradeDetailsWithAverage( - subject = summary.subject, - average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) { - grades.updateModifiers(student).calcAverage() - } else summary.average, - points = summary.pointsSum, - summary = summary, - grades = grades - ) - } - } - } - - private fun List.emulateEmptySummaries(student: Student, semester: Semester, grades: List>>, calcAverage: Boolean): List { - if (isNotEmpty() && size > grades.size) return this - - return grades.mapIndexed { i, (subject, details) -> - singleOrNull { it.subject == subject }?.let { return@mapIndexed it } - GradeSummary( - studentId = student.studentId, - semesterId = semester.semesterId, - position = i, - subject = subject, - predictedGrade = "", - finalGrade = "", - proposedPoints = "", - finalPoints = "", - pointsSum = "", - average = if (calcAverage) details.updateModifiers(student).calcAverage() else .0 - ) - } - } - - private fun List.updateModifiers(student: Student): List { - return if (student.loginMode == Sdk.Mode.SCRAPPER.name) { - map { it.changeModifier(plusModifier, minusModifier) } - } else this + 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 } } } 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 deleted file mode 100644 index 3f5d706b3..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeDetailsWithAverage.kt +++ /dev/null @@ -1,12 +0,0 @@ -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 59b83c4b0..b1faf18f1 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,15 +1,16 @@ 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 @@ -18,9 +19,10 @@ 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(R.layout.fragment_grade), GradeView, MainView.MainChildView, MainView.TitledView { +class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainView.TitledView { @Inject lateinit var presenter: GradePresenter @@ -40,16 +42,19 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade override var subtitleString = "" - override val currentPageIndex get() = binding.gradeViewPager.currentItem + override val currentPageIndex get() = gradeViewPager.currentItem override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentGradeBinding.bind(view) + 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) presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY)) } @@ -61,7 +66,7 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade override fun initView() { with(pagerAdapter) { - containerId = binding.gradeViewPager.id + containerId = gradeViewPager.id addFragmentsWithTitle(mapOf( GradeDetailsFragment.newInstance() to getString(R.string.all_details), GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), @@ -69,21 +74,19 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade )) } - with(binding.gradeViewPager) { + with(gradeViewPager) { adapter = pagerAdapter offscreenPageLimit = 3 setOnSelectPageListener(presenter::onPageSelected) } - with(binding.gradeTabLayout) { - setupWithViewPager(binding.gradeViewPager) + with(gradeTabLayout) { + setupWithViewPager(gradeViewPager) setElevationCompat(context.dpToPx(4f)) } - with(binding) { - gradeErrorRetry.setOnClickListener { presenter.onRetry() } - gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() } - } + gradeErrorRetry.setOnClickListener { presenter.onRetry() } + gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -96,22 +99,20 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade } override fun showContent(show: Boolean) { - with(binding) { - gradeViewPager.visibility = if (show) VISIBLE else INVISIBLE - gradeTabLayout.visibility = if (show) VISIBLE else INVISIBLE - } + gradeViewPager.visibility = if (show) VISIBLE else INVISIBLE + gradeTabLayout.visibility = if (show) VISIBLE else INVISIBLE } override fun showProgress(show: Boolean) { - binding.gradeProgress.visibility = if (show) VISIBLE else INVISIBLE + gradeProgress.visibility = if (show) VISIBLE else INVISIBLE } override fun showErrorView(show: Boolean) { - binding.gradeError.visibility = if (show) VISIBLE else INVISIBLE + gradeError.visibility = if (show) VISIBLE else INVISIBLE } override fun setErrorDetails(message: String) { - binding.gradeErrorMessage.text = message + gradeErrorMessage.text = message } override fun showSemesterSwitch(show: Boolean) { @@ -165,7 +166,7 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt index ec66e2bd9..78885ebd5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt @@ -133,7 +133,6 @@ class GradePresenter @Inject constructor( } private fun loadChild(index: Int, forceRefresh: Boolean = false) { - Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}") semesters.first { it.semesterName == selectedIndex }.semesterId.also { if (forceRefresh || loadedSemesterId[index] != it) { Timber.i("Load grade child view index: $index") 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 deleted file mode 100644 index c129d9485..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt +++ /dev/null @@ -1,198 +0,0 @@ -package io.github.wulkanowy.ui.modules.grade.details - -import android.annotation.SuppressLint -import android.content.res.Resources -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.NO_POSITION -import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.Grade -import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding -import io.github.wulkanowy.databinding.ItemGradeDetailsBinding -import io.github.wulkanowy.ui.base.BaseExpandableAdapter -import io.github.wulkanowy.utils.getBackgroundColor -import io.github.wulkanowy.utils.toFormattedString -import timber.log.Timber -import javax.inject.Inject - -class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() { - - private var headers = mutableListOf() - - private var items = mutableListOf() - - private var expandedPosition = 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 = NO_POSITION - } - - fun updateDetailsItem(position: Int, grade: Grade) { - if (items.getOrNull(position)?.viewType != ViewType.ITEM) { - Timber.e("Trying to update item $position on list ${items.size} size, expanded position: $expandedPosition") - return - } - items[position] = GradeDetailsItem(grade, ViewType.ITEM) - notifyItemChanged(position) - } - - fun getHeaderItem(subject: String): GradeDetailsItem { - val candidates = headers.filter { (it.value as GradeDetailsHeader).subject == subject } - - if (candidates.size > 1) { - Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates") - } - - return candidates.first() - } - - fun updateHeaderItem(item: GradeDetailsItem) { - val headerPosition = headers.indexOf(item) - val itemPosition = items.indexOf(item) - - if (headerPosition == NO_POSITION || itemPosition == NO_POSITION) { - Timber.e("Invalid update header positions! Header: $headerPosition, item: $itemPosition") - } - - headers[headerPosition] = item - items[itemPosition] = item - notifyItemChanged(itemPosition) - } - - fun collapseAll() { - if (expandedPosition != -1) { - refreshList(headers) - expandedPosition = NO_POSITION - } - } - - @Synchronized - private fun refreshList(newItems: MutableList) { - val diffCallback = GradeDetailsDiffUtil(items, newItems) - val diffResult = DiffUtil.calculateDiff(diffCallback) - items = newItems - diffResult.dispatchUpdatesTo(this) - } - - override fun getItemCount() = items.size - - override fun getItemViewType(position: Int) = items[position].viewType.id - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val inflater = LayoutInflater.from(parent.context) - - return when (viewType) { - ViewType.HEADER.id -> HeaderViewHolder(HeaderGradeDetailsBinding.inflate(inflater, parent, false)) - ViewType.ITEM.id -> ItemViewHolder(ItemGradeDetailsBinding.inflate(inflater, parent, false)) - else -> throw IllegalStateException() - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is HeaderViewHolder -> bindHeaderViewHolder( - holder = holder, - header = items[position].value as GradeDetailsHeader, - position = position - ) - is ItemViewHolder -> bindItemViewHolder( - holder = holder, - grade = items[position].value as Grade - ) - } - } - - private fun bindHeaderViewHolder(holder: HeaderViewHolder, header: GradeDetailsHeader, position: Int) { - val headerPosition = headers.indexOf(items[position]) - val adapterPosition = holder.adapterPosition - - with(holder.binding) { - gradeHeaderDivider.visibility = if (adapterPosition == 0) View.GONE else View.VISIBLE - with(gradeHeaderSubject) { - text = header.subject - maxLines = if (headerPosition == expandedPosition) 2 else 1 - } - gradeHeaderAverage.text = formatAverage(header.average, root.context.resources) - gradeHeaderPointsSum.text = root.context.getString(R.string.grade_points_sum, header.pointsSum) - gradeHeaderPointsSum.visibility = if (!header.pointsSum.isNullOrEmpty()) View.VISIBLE else View.GONE - gradeHeaderNumber.text = root.context.resources.getQuantityString(R.plurals.grade_number_item, header.grades.size, header.grades.size) - gradeHeaderNote.visibility = if (header.newGrades > 0) View.VISIBLE else View.GONE - if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10) - - gradeHeaderContainer.isEnabled = isExpandable - gradeHeaderContainer.setOnClickListener { - expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition - - if (expandedPosition != NO_POSITION) { - refreshList(headers.toMutableList().apply { - addAll(headerPosition + 1, header.grades) - }) - scrollToHeaderWithSubItems(headerPosition, header.grades.size) - } else { - refreshList(headers) - } - } - } - } - - private fun formatAverage(average: Double?, resources: Resources): String { - return if (average == null || average == .0) resources.getString(R.string.grade_no_average) - else resources.getString(R.string.grade_average, average) - } - - @SuppressLint("SetTextI18n") - private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) { - with(holder.binding) { - gradeItemValue.run { - text = grade.entry - setBackgroundResource(grade.getBackgroundColor(colorTheme)) - } - gradeItemDescription.text = when { - grade.description.isNotBlank() -> grade.description - grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol - else -> root.context.getString(R.string.all_no_description) - } - gradeItemDate.text = grade.date.toFormattedString() - gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}" - gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE - - root.setOnClickListener { - holder.adapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) } - } - } - } - - private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) : - RecyclerView.ViewHolder(binding.root) - - private class ItemViewHolder(val binding: ItemGradeDetailsBinding) : - RecyclerView.ViewHolder(binding.root) - - class GradeDetailsDiffUtil(private val old: List, 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 698aff3ea..a8f8a8653 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,17 +8,14 @@ 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 @@ -47,49 +44,47 @@ class GradeDetailsDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogGradeBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_grade, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - with(binding) { - gradeDialogSubject.text = grade.subject + 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 - } - } - - gradeDialogClose.setOnClickListener { dismiss() } + 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 + } + } + + 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 ac50b9f53..9505d354f 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,6 +1,7 @@ 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 @@ -8,25 +9,29 @@ import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import androidx.recyclerview.widget.LinearLayoutManager +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 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(R.layout.fragment_grade_details), GradeDetailsView, - GradeView.GradeChildView { +class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeChildView { @Inject lateinit var presenter: GradeDetailsPresenter @Inject - lateinit var gradeDetailsAdapter: GradeDetailsAdapter + lateinit var gradeDetailsAdapter: FlexibleAdapter> private var gradeDetailsMenu: Menu? = null @@ -34,18 +39,36 @@ class GradeDetailsFragment : fun newInstance() = GradeDetailsFragment() } + override val emptyAverageString: String + get() = getString(R.string.grade_no_average) + + override val averageString: String + get() = getString(R.string.grade_average) + + override val pointsSumString: String + get() = getString(R.string.grade_points_sum) + + override val weightString: String + get() = getString(R.string.grade_weight) + + override val noDescriptionString: String + get() = getString(R.string.all_no_description) + override val isViewEmpty - get() = gradeDetailsAdapter.itemCount == 0 + get() = gradeDetailsAdapter.isEmpty override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentGradeDetailsBinding.bind(view) - messageContainer = binding.gradeDetailsRecycler + 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 presenter.onAttachView(this) } @@ -56,17 +79,22 @@ class GradeDetailsFragment : } override fun initView() { - gradeDetailsAdapter.onClickListener = presenter::onGradeItemSelected - - with(binding) { - with(gradeDetailsRecycler) { - layoutManager = LinearLayoutManager(context) - adapter = gradeDetailsAdapter - } - gradeDetailsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - gradeDetailsErrorRetry.setOnClickListener { presenter.onRetry() } - gradeDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() } + gradeDetailsAdapter.run { + isAutoCollapseOnExpand = true + isAutoScrollOnExpand = true + setOnItemClickListener { presenter.onGradeItemSelected(it) } } + + gradeDetailsRecycler.run { + layoutManager = SmoothScrollLinearLayoutManager(context) + adapter = gradeDetailsAdapter + addItemDecoration(GradeDetailsHeaderItemDecoration(context) + .withDefaultDivider(R.layout.header_grade_details) + ) + } + gradeDetailsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + gradeDetailsErrorRetry.setOnClickListener { presenter.onRetry() } + gradeDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -74,23 +102,16 @@ class GradeDetailsFragment : else false } - override fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) { - with(gradeDetailsAdapter) { - colorTheme = gradeColorTheme - setDataItems(data, isGradeExpandable) - notifyDataSetChanged() - } + override fun updateData(data: List) { + gradeDetailsAdapter.updateDataSet(data, true) } - override fun updateItem(item: Grade, position: Int) { - gradeDetailsAdapter.updateDetailsItem(position, item) + override fun updateItem(item: AbstractFlexibleItem<*>) { + gradeDetailsAdapter.updateItem(item) } override fun clearView() { - with(gradeDetailsAdapter) { - setDataItems(mutableListOf()) - notifyDataSetChanged() - } + gradeDetailsAdapter.clear() } override fun collapseAllItems() { @@ -98,43 +119,43 @@ class GradeDetailsFragment : } override fun scrollToStart() { - binding.gradeDetailsRecycler.smoothScrollToPosition(0) + gradeDetailsRecycler.scrollToPosition(0) } - override fun getHeaderOfItem(subject: String): GradeDetailsItem { - return gradeDetailsAdapter.getHeaderItem(subject) + override fun getHeaderOfItem(item: AbstractFlexibleItem<*>): IExpandable<*, out IFlexible<*>>? { + return gradeDetailsAdapter.getExpandableOf(item) } - override fun updateHeaderItem(item: GradeDetailsItem) { - gradeDetailsAdapter.updateHeaderItem(item) + override fun getGradeNumberString(number: Int): String { + return resources.getQuantityString(R.plurals.grade_number_item, number, number) } override fun showProgress(show: Boolean) { - binding.gradeDetailsProgress.visibility = if (show) VISIBLE else GONE + gradeDetailsProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.gradeDetailsSwipe.isEnabled = enable + gradeDetailsSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.gradeDetailsRecycler.visibility = if (show) VISIBLE else INVISIBLE + gradeDetailsRecycler.visibility = if (show) VISIBLE else INVISIBLE } override fun showEmpty(show: Boolean) { - binding.gradeDetailsEmpty.visibility = if (show) VISIBLE else INVISIBLE + gradeDetailsEmpty.visibility = if (show) VISIBLE else INVISIBLE } override fun showErrorView(show: Boolean) { - binding.gradeDetailsError.visibility = if (show) VISIBLE else GONE + gradeDetailsError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.gradeDetailsErrorMessage.text = message + gradeDetailsErrorMessage.text = message } override fun showRefresh(show: Boolean) { - binding.gradeDetailsSwipe.isRefreshing = show + gradeDetailsSwipe.isRefreshing = show } override fun showGradeDialog(grade: Grade, colorScheme: String) { @@ -166,7 +187,7 @@ class GradeDetailsFragment : } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } } 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 new file mode 100644 index 000000000..4a34a1457 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsHeader.kt @@ -0,0 +1,94 @@ +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 new file mode 100644 index 000000000..39a911e62 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsHeaderItemDecoration.kt @@ -0,0 +1,38 @@ +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 281974969..1e47eca5d 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,19 +1,74 @@ package io.github.wulkanowy.ui.modules.grade.details -enum class ViewType(val id: Int) { - HEADER(1), - ITEM(2) +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 + } } - -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 37f2c935a..a9e5b2b7d 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,5 +1,6 @@ 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 @@ -8,9 +9,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 @@ -42,20 +43,24 @@ class GradeDetailsPresenter @Inject constructor( loadData(semesterId, forceRefresh) } - fun onGradeItemSelected(grade: Grade, position: Int) { - Timber.i("Select grade item ${grade.id}, position: $position") - 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) + 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) } - newGradesAmount-- - updateMarkAsDoneButton() - updateGrade(grade) } } } @@ -127,7 +132,16 @@ class GradeDetailsPresenter @Inject constructor( private fun loadData(semesterId: Int, forceRefresh: Boolean) { Timber.i("Loading grade details data started") disposable.add(studentRepository.getCurrentStudent() - .flatMap { averageProvider.getGradesDetailsWithAverage(it, semesterId, forceRefresh) } + .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) } + } + } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -138,26 +152,17 @@ class GradeDetailsPresenter @Inject constructor( notifyParentDataLoaded(semesterId) } } - .subscribe({ grades -> + .subscribe({ Timber.i("Loading grade details result: Success") - newGradesAmount = grades.sumBy { it.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } } + newGradesAmount = it.sumBy { gradeDetailsHeader -> gradeDetailsHeader.newGrades } updateMarkAsDoneButton() view?.run { - showEmpty(grades.isEmpty()) + showEmpty(it.isEmpty()) showErrorView(false) - showContent(grades.isNotEmpty()) - updateData( - data = createGradeItems(grades), - isGradeExpandable = preferencesRepository.isGradeExpandable, - gradeColorTheme = preferencesRepository.gradeColorTheme - ) + showContent(it.isNotEmpty()) + updateData(it) } - analytics.logEvent( - "load_data", - "type" to "grade_details", - "items" to grades.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_grade_details", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading grade details result: An exception occurred") errorHandler.dispatch(it) @@ -175,23 +180,40 @@ class GradeDetailsPresenter @Inject constructor( } } - private fun createGradeItems(items: List): List { - return items - .filter { it.grades.isNotEmpty() } - .sortedBy { it.subject } - .map { (subject, average, points, _, grades) -> - val subItems = grades - .sortedByDescending { it.date } - .map { GradeDetailsItem(it, ViewType.ITEM) } + private fun createGradeItems(items: Map>, averages: List>): List { + val isGradeExpandable = preferencesRepository.isGradeExpandable + val gradeColorTheme = preferencesRepository.gradeColorTheme - 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() + val noDescriptionString = view?.noDescriptionString.orEmpty() + val weightString = view?.weightString.orEmpty() + val pointsSumString = view?.pointsSumString.orEmpty() + + return items.map { subject -> + GradeDetailsHeader( + subject = subject.key, + average = formatAverage(averages.singleOrNull { subject.key == it.first }?.second), + pointsSum = averages.singleOrNull { subject.key == it.first }?.takeIf { it.third.isNotEmpty() }?.let { pointsSumString.format(it.third) }.orEmpty(), + number = view?.getGradeNumberString(subject.value.size).orEmpty(), + newGrades = subject.value.filter { grade -> !grade.isRead }.size, + isExpandable = isGradeExpandable + ).apply { + subItems = subject.value.map { item -> + GradeDetailsItem( + grade = item, + valueBgColor = item.getBackgroundColor(gradeColorTheme), + weightString = weightString, + noDescriptionString = noDescriptionString + ) + } + } + } + } + + private fun formatAverage(average: Double?): String { + return view?.run { + if (average == null || average == .0) emptyAverageString + else averageString.format(average) + }.orEmpty() } private fun updateGrade(grade: Grade) { @@ -199,9 +221,8 @@ 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 e71fcc3c8..e2977bcbe 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,5 +1,8 @@ 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 @@ -7,13 +10,21 @@ 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, isGradeExpandable: Boolean, gradeColorTheme: String) + fun updateData(data: List) - fun updateItem(item: Grade, position: Int) - - fun updateHeaderItem(item: GradeDetailsItem) + fun updateItem(item: AbstractFlexibleItem<*>) fun clearView() @@ -43,5 +54,7 @@ interface GradeDetailsView : BaseView { fun enableMarkAsDoneButton(enable: Boolean) - fun getHeaderOfItem(subject: String): GradeDetailsItem + fun getGradeNumberString(number: Int): String + + fun getHeaderOfItem(item: AbstractFlexibleItem<*>): IExpandable<*, out IFlexible<*>>? } 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 dbb60910a..a7b7b6534 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,6 +2,7 @@ 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 @@ -20,9 +21,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() : @@ -32,8 +33,6 @@ 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, @@ -61,30 +60,34 @@ class GradeStatisticsAdapter @Inject constructor() : "6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+" ) - override fun getItemCount() = if (showAllSubjectsOnList) items.size else (if (items.isEmpty()) 0 else 1) + override fun getItemCount() = items.size - override fun getItemViewType(position: Int) = items[position].type.id + 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 onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val inflater = LayoutInflater.from(parent.context) + val viewHolder = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return when (viewType) { - ViewType.PARTIAL.id, ViewType.SEMESTER.id -> PieViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)) - ViewType.POINTS.id -> BarViewHolder(ItemGradeStatisticsBarBinding.inflate(inflater, parent, false)) - else -> throw IllegalStateException() + R.layout.item_grade_statistics_bar -> GradeStatisticsBar(viewHolder) + else -> GradeStatisticsPie(viewHolder) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is PieViewHolder -> bindPieChart(holder, items[position].partial) - is BarViewHolder -> bindBarChart(holder, items[position].points!!) + is GradeStatisticsPie -> bindPieChart(holder, items[position].partial) + is GradeStatisticsBar -> bindBarChart(holder, items[position].points!!) } } - private fun bindPieChart(holder: PieViewHolder, partials: List) { - with(holder.binding.gradeStatisticsPieTitle) { + private fun bindPieChart(holder: GradeStatisticsPie, partials: List) { + with(holder.view.gradeStatisticsPieTitle) { text = partials.firstOrNull()?.subject - visibility = if (items.size == 1 || !showAllSubjectsOnList) GONE else VISIBLE + visibility = if (items.size == 1) GONE else VISIBLE } val gradeColors = when (theme) { @@ -102,10 +105,10 @@ class GradeStatisticsAdapter @Inject constructor() : valueTextColor = Color.WHITE setColors(partials.map { gradeColors.single { color -> color.first == it.grade }.second - }.toIntArray(), holder.binding.root.context) + }.toIntArray(), holder.view.context) } - with(holder.binding.gradeStatisticsPie) { + with(holder.view.gradeStatisticsPie) { setTouchEnabled(false) if (partials.size == 1) animateXY(1000, 1000) data = PieData(dataset).apply { @@ -137,8 +140,8 @@ class GradeStatisticsAdapter @Inject constructor() : } } - private fun bindBarChart(holder: BarViewHolder, points: GradePointsStatistics) { - with(holder.binding.gradeStatisticsBarTitle) { + private fun bindBarChart(holder: GradeStatisticsBar, points: GradePointsStatistics) { + with(holder.view.gradeStatisticsBarTitle) { text = points.subject visibility = if (items.size == 1) GONE else VISIBLE } @@ -150,14 +153,14 @@ class GradeStatisticsAdapter @Inject constructor() : with(dataset) { valueTextSize = 12f - valueTextColor = holder.binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary) + valueTextColor = holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary) valueFormatter = object : ValueFormatter() { override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%" } colors = gradePointsColors } - with(holder.binding.gradeStatisticsBar) { + with(holder.view.gradeStatisticsBar) { setTouchEnabled(false) if (items.size == 1) animateXY(1000, 1000) data = BarData(dataset).apply { @@ -180,7 +183,7 @@ class GradeStatisticsAdapter @Inject constructor() : description.isEnabled = false - holder.binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary).let { + holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary).let { axisLeft.textColor = it axisRight.textColor = it } @@ -200,9 +203,7 @@ class GradeStatisticsAdapter @Inject constructor() : } } - private class PieViewHolder(val binding: ItemGradeStatisticsPieBinding) : - RecyclerView.ViewHolder(binding.root) + class GradeStatisticsPie(val view: View) : RecyclerView.ViewHolder(view) - private class BarViewHolder(val binding: ItemGradeStatisticsBarBinding) : - RecyclerView.ViewHolder(binding.root) + class GradeStatisticsBar(val view: View) : RecyclerView.ViewHolder(view) } 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 3a8f40073..c05cf3800 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(R.layout.fragment_grade_statistics), - GradeStatisticsView, GradeView.GradeChildView { +class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.GradeChildView { @Inject lateinit var presenter: GradeStatisticsPresenter @@ -36,21 +36,24 @@ class GradeStatisticsFragment : override val isViewEmpty get() = statisticsAdapter.items.isEmpty() override val currentType - get() = when (binding.gradeStatisticsTypeSwitch.checkedRadioButtonId) { + get() = when (gradeStatisticsTypeSwitch.checkedRadioButtonId) { R.id.gradeStatisticsTypeSemester -> ViewType.SEMESTER R.id.gradeStatisticsTypePartial -> ViewType.PARTIAL else -> ViewType.POINTS } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentGradeStatisticsBinding.bind(view) - messageContainer = binding.gradeStatisticsSwipe + 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 presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? ViewType) } override fun initView() { - with(binding.gradeStatisticsRecycler) { + with(gradeStatisticsRecycler) { layoutManager = LinearLayoutManager(requireContext()) adapter = statisticsAdapter } @@ -58,18 +61,16 @@ class GradeStatisticsFragment : subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject) - with(binding.gradeStatisticsSubjects) { + with(gradeStatisticsSubjects) { adapter = subjectsAdapter setOnItemSelectedListener { presenter.onSubjectSelected(it?.text?.toString()) } } - with(binding) { - gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + 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) { @@ -80,17 +81,15 @@ class GradeStatisticsFragment : } } - override fun updateData(items: List, theme: String, showAllSubjectsOnStatisticsList: Boolean) { - with(statisticsAdapter) { - this.showAllSubjectsOnList = showAllSubjectsOnStatisticsList - this.theme = theme - this.items = items - notifyDataSetChanged() - } + override fun updateData(items: List, theme: String) { + statisticsAdapter.theme = theme + statisticsAdapter.items = items + statisticsAdapter.notifyDataSetChanged() } override fun showSubjects(show: Boolean) { - binding.gradeStatisticsSubjectsContainer.visibility = if (show) View.VISIBLE else View.GONE + gradeStatisticsSubjectsContainer.visibility = if (show) View.VISIBLE else View.INVISIBLE + gradeStatisticsTypeSwitch.visibility = if (show) View.VISIBLE else View.INVISIBLE } override fun clearView() { @@ -98,35 +97,35 @@ class GradeStatisticsFragment : } override fun resetView() { - binding.gradeStatisticsScroll.scrollTo(0, 0) + gradeStatisticsScroll.scrollTo(0, 0) } override fun showContent(show: Boolean) { - binding.gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE + gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE } override fun showEmpty(show: Boolean) { - binding.gradeStatisticsEmpty.visibility = if (show) View.VISIBLE else View.INVISIBLE + gradeStatisticsEmpty.visibility = if (show) View.VISIBLE else View.INVISIBLE } override fun showErrorView(show: Boolean) { - binding.gradeStatisticsError.visibility = if (show) View.VISIBLE else View.GONE + gradeStatisticsError.visibility = if (show) View.VISIBLE else View.GONE } override fun setErrorDetails(message: String) { - binding.gradeStatisticsErrorMessage.text = message + gradeStatisticsErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.gradeStatisticsProgress.visibility = if (show) View.VISIBLE else View.GONE + gradeStatisticsProgress.visibility = if (show) View.VISIBLE else View.GONE } override fun enableSwipe(enable: Boolean) { - binding.gradeStatisticsSwipe.isEnabled = enable + gradeStatisticsSwipe.isEnabled = enable } override fun showRefresh(show: Boolean) { - binding.gradeStatisticsSwipe.isRefreshing = show + gradeStatisticsSwipe.isRefreshing = show } override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { @@ -151,7 +150,7 @@ class GradeStatisticsFragment : override fun onResume() { super.onResume() - binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() } + gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() } } override fun onSaveInstanceState(outState: Bundle) { @@ -160,7 +159,7 @@ class GradeStatisticsFragment : } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } } 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 590e9ce12..90c4e38ef 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,7 +128,10 @@ class GradeStatisticsPresenter @Inject constructor( .observeOn(schedulers.mainThread) .subscribe({ Timber.i("Loading grade stats subjects result: Success") - view?.updateSubjects(it) + view?.run { + updateSubjects(it) + showSubjects(true) + } }, { Timber.i("Loading grade stats subjects result: An exception occurred") errorHandler.dispatch(it) @@ -137,21 +140,22 @@ class GradeStatisticsPresenter @Inject constructor( } private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) { - currentSubjectName = if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName + currentSubjectName = 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 } - 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) - } + 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) } } } @@ -171,15 +175,9 @@ class GradeStatisticsPresenter @Inject constructor( showEmpty(it.isEmpty()) showContent(it.isNotEmpty()) showErrorView(false) - updateData(it, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList) - showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) + updateData(it, preferencesRepository.gradeColorTheme) } - analytics.logEvent( - "load_data", - "type" to "grade_statistics", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_grade_statistics", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading grade stats result: An exception occurred") errorHandler.dispatch(it) 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 26b4a119a..9ba8524c2 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, showAllSubjectsOnStatisticsList: Boolean) + fun updateData(items: List, theme: String) 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 02e95b0e5..08c37d587 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(val id: Int) { - SEMESTER(1), - PARTIAL(2), - POINTS(3) +enum class ViewType { + SEMESTER, + PARTIAL, + POINTS } 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 deleted file mode 100644 index 30c4ccc23..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt +++ /dev/null @@ -1,85 +0,0 @@ -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 d169f7c62..05fde5227 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,35 +1,36 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import android.view.ViewGroup +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.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(R.layout.fragment_grade_summary), GradeSummaryView, - GradeView.GradeChildView { +class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeChildView { @Inject lateinit var presenter: GradeSummaryPresenter @Inject - lateinit var gradeSummaryAdapter: GradeSummaryAdapter + lateinit var gradeSummaryAdapter: FlexibleAdapter> companion object { fun newInstance() = GradeSummaryFragment() } override val isViewEmpty - get() = gradeSummaryAdapter.items.isEmpty() + get() = gradeSummaryAdapter.isEmpty override val predictedString get() = getString(R.string.grade_summary_predicted_grade) @@ -37,69 +38,70 @@ class GradeSummaryFragment : override val finalString get() = getString(R.string.grade_summary_final_grade) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentGradeSummaryBinding.bind(view) - messageContainer = binding.gradeSummaryRecycler + 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 presenter.onAttachView(this) } override fun initView() { - with(binding.gradeSummaryRecycler) { - layoutManager = LinearLayoutManager(context) + gradeSummaryAdapter.setDisplayHeadersAtStartUp(true) + + gradeSummaryRecycler.run { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = gradeSummaryAdapter } - with(binding) { - gradeSummarySwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() } - gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } - } + gradeSummarySwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() } + gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - override fun updateData(data: List) { - with(gradeSummaryAdapter) { - items = data - notifyDataSetChanged() + override fun updateData(data: List, header: GradeSummaryScrollableHeader) { + gradeSummaryAdapter.apply { + updateDataSet(data, true) + removeAllScrollableHeaders() + addScrollableHeader(header) } } override fun clearView() { - with(gradeSummaryAdapter) { - items = emptyList() - notifyDataSetChanged() - } + gradeSummaryAdapter.clear() } override fun resetView() { - binding.gradeSummaryRecycler.scrollToPosition(0) + gradeSummaryRecycler.scrollToPosition(0) } override fun showContent(show: Boolean) { - binding.gradeSummaryRecycler.visibility = if (show) VISIBLE else INVISIBLE + gradeSummaryRecycler.visibility = if (show) VISIBLE else INVISIBLE } override fun showEmpty(show: Boolean) { - binding.gradeSummaryEmpty.visibility = if (show) VISIBLE else INVISIBLE + gradeSummaryEmpty.visibility = if (show) VISIBLE else INVISIBLE } override fun showErrorView(show: Boolean) { - binding.gradeSummaryError.visibility = if (show) VISIBLE else INVISIBLE + gradeSummaryError.visibility = if (show) VISIBLE else INVISIBLE } override fun setErrorDetails(message: String) { - binding.gradeSummaryErrorMessage.text = message + gradeSummaryErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.gradeSummaryProgress.visibility = if (show) VISIBLE else GONE + gradeSummaryProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.gradeSummarySwipe.isEnabled = enable + gradeSummarySwipe.isEnabled = enable } override fun showRefresh(show: Boolean) { - binding.gradeSummarySwipe.isRefreshing = show + gradeSummarySwipe.isRefreshing = show } override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { @@ -123,7 +125,7 @@ class GradeSummaryFragment : } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } } 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 new file mode 100644 index 000000000..95c32d176 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt @@ -0,0 +1,64 @@ +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 229a0107d..c12f2a516 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,20 +1,26 @@ 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) { @@ -30,8 +36,15 @@ class GradeSummaryPresenter @Inject constructor( fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) { Timber.i("Loading grade summary data started") disposable.add(studentRepository.getCurrentStudent() - .flatMap { averageProvider.getGradesDetailsWithAverage(it, semesterId, forceRefresh) } - .map { createGradeSummaryItems(it) } + .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) } + } + } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -41,20 +54,15 @@ class GradeSummaryPresenter @Inject constructor( enableSwipe(true) notifyParentDataLoaded(semesterId) } - }.subscribe({ + }.subscribe({ (gradeSummaryItems, gradeSummaryHeader) -> Timber.i("Loading grade summary result: Success") view?.run { - showEmpty(it.isEmpty()) - showContent(it.isNotEmpty()) + showEmpty(gradeSummaryItems.isEmpty()) + showContent(gradeSummaryItems.isNotEmpty()) showErrorView(false) - updateData(it) + updateData(gradeSummaryItems, gradeSummaryHeader) } - analytics.logEvent( - "load_data", - "type" to "grade_summary", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_grade_summary", "items" to gradeSummaryItems.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading grade summary result: An exception occurred") errorHandler.dispatch(it) @@ -107,19 +115,31 @@ class GradeSummaryPresenter @Inject constructor( disposable.clear() } - private fun createGradeSummaryItems(items: List): List { - return items - .filter { !checkEmpty(it) } - .sortedBy { it.subject } - .map { it.summary.copy(average = it.average) } + 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 checkEmpty(gradeSummary: GradeDetailsWithAverage): Boolean { + private fun checkEmpty(gradeSummary: GradeSummary, averages: List>): Boolean { return gradeSummary.run { - summary.finalGrade.isBlank() - && summary.predictedGrade.isBlank() - && average == .0 - && points.isBlank() + 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 new file mode 100644 index 000000000..f1c535c71 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryScrollableHeader.kt @@ -0,0 +1,53 @@ +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 974d91415..cf3184873 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,6 +1,5 @@ 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 { @@ -13,7 +12,7 @@ interface GradeSummaryView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List, header: GradeSummaryScrollableHeader) 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 deleted file mode 100644 index a87ad18e8..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkAdapter.kt +++ /dev/null @@ -1,67 +0,0 @@ -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 f30529575..a011a015c 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,29 +1,33 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +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 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(R.layout.fragment_homework), - HomeworkView, MainView.TitledView { +class HomeworkFragment : BaseFragment(), HomeworkView, MainView.TitledView { @Inject lateinit var presenter: HomeworkPresenter @Inject - lateinit var homeworkAdapter: HomeworkAdapter + lateinit var homeworkAdapter: FlexibleAdapter> companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" @@ -33,41 +37,41 @@ class HomeworkFragment : BaseFragment(R.layout.fragment override val titleStringId get() = R.string.homework_title - override val isViewEmpty get() = homeworkAdapter.items.isEmpty() + override val isViewEmpty get() = homeworkAdapter.isEmpty - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentHomeworkBinding.bind(view) - messageContainer = binding.homeworkRecycler + 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 presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - homeworkAdapter.onClickListener = presenter::onHomeworkItemSelected + homeworkAdapter.setOnItemClickListener(presenter::onHomeworkItemSelected) - with(binding.homeworkRecycler) { - layoutManager = LinearLayoutManager(context) + with(homeworkRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = homeworkAdapter - addItemDecoration(DividerItemDecoration(context)) + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider() + .withDrawDividerOnLastItem(false)) } - with(binding) { - homeworkSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - homeworkErrorRetry.setOnClickListener { presenter.onRetry() } - homeworkErrorDetails.setOnClickListener { presenter.onDetailsClick() } + 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>) { - with(homeworkAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + homeworkAdapter.updateDataSet(data, true) } fun onReloadList() { @@ -75,50 +79,47 @@ class HomeworkFragment : BaseFragment(R.layout.fragment } override fun clearData() { - with(homeworkAdapter) { - items = emptyList() - notifyDataSetChanged() - } + homeworkAdapter.clear() } override fun updateNavigationWeek(date: String) { - binding.homeworkNavDate.text = date + homeworkNavDate.text = date } override fun hideRefresh() { - binding.homeworkSwipe.isRefreshing = false + homeworkSwipe.isRefreshing = false } override fun showEmpty(show: Boolean) { - binding.homeworkEmpty.visibility = if (show) VISIBLE else GONE + homeworkEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.homeworkError.visibility = if (show) VISIBLE else GONE + homeworkError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.homeworkErrorMessage.text = message + homeworkErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.homeworkProgress.visibility = if (show) VISIBLE else GONE + homeworkProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.homeworkSwipe.isEnabled = enable + homeworkSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.homeworkRecycler.visibility = if (show) VISIBLE else GONE + homeworkRecycler.visibility = if (show) VISIBLE else GONE } override fun showPreButton(show: Boolean) { - binding.homeworkPreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE + homeworkPreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE } override fun showNextButton(show: Boolean) { - binding.homeworkNextButton.visibility = if (show) VISIBLE else View.INVISIBLE + 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 new file mode 100644 index 000000000..490237883 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkHeader.kt @@ -0,0 +1,54 @@ +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 7e0039583..3c2dd7baf 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,9 +1,53 @@ package io.github.wulkanowy.ui.modules.homework -data class HomeworkItem(val value: T, val viewType: ViewType) { +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.* - enum class ViewType(val id: Int) { - HEADER(1), - ITEM(2) +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 } } 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 41735a23f..aae18df32 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,5 +1,6 @@ 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 @@ -8,7 +9,7 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.SchedulersProvider -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.monday @@ -17,6 +18,7 @@ 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( @@ -72,9 +74,11 @@ class HomeworkPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - fun onHomeworkItemSelected(homework: Homework) { - Timber.i("Select homework item ${homework.id}") - view?.showTimetableDialog(homework) + fun onHomeworkItemSelected(item: AbstractFlexibleItem<*>?) { + if (item is HomeworkItem) { + Timber.i("Select homework item ${item.homework.id}") + view?.showTimetableDialog(item.homework) + } } private fun setBaseDateOnHolidays() { @@ -106,6 +110,8 @@ 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) @@ -124,12 +130,7 @@ class HomeworkPresenter @Inject constructor( showErrorView(false) showContent(it.isNotEmpty()) } - analytics.logEvent( - "load_data", - "type" to "homework", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_homework", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading homework result: An exception occurred") @@ -149,12 +150,12 @@ class HomeworkPresenter @Inject constructor( } } - 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) + private fun createHomeworkItem(items: Map>): List { + return items.flatMap { + HomeworkHeader(it.key).let { header -> + it.value.reversed().map { item -> HomeworkItem(header, item) } } - }.flatten() + } } private fun reloadView() { @@ -175,7 +176,7 @@ class HomeworkPresenter @Inject constructor( showPreButton(!currentDate.minusDays(7).isHolidays) showNextButton(!currentDate.plusDays(7).isHolidays) updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " + - currentDate.sunday.toFormattedString("dd.MM")) + currentDate.friday.toFormattedString("dd.MM")) } } } 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 2a678cd4c..1d241df46 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 5d6ee162a..3706b56e1 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,15 +1,14 @@ package io.github.wulkanowy.ui.modules.homework.details import android.view.LayoutInflater -import android.view.View.GONE -import android.view.View.VISIBLE +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.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() : @@ -31,10 +30,6 @@ 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) { @@ -47,9 +42,9 @@ class HomeworkDetailsAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - 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)) + 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)) } } @@ -61,42 +56,29 @@ class HomeworkDetailsAdapter @Inject constructor() : } private fun bindDetailsViewHolder(holder: DetailsViewHolder) { - with(holder.binding) { + with(holder.view) { 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.binding) { + with(holder.view) { homeworkDialogAttachment.text = item.second - root.setOnClickListener { + setOnClickListener { onAttachmentClickListener(item.first) } } } - class DetailsViewHolder(val binding: ItemHomeworkDialogDetailsBinding) : - RecyclerView.ViewHolder(binding.root) + class DetailsViewHolder(val view: View) : RecyclerView.ViewHolder(view) - class AttachmentsHeaderViewHolder(val binding: ItemHomeworkDialogAttachmentsHeaderBinding) : - RecyclerView.ViewHolder(binding.root) + class AttachmentsHeaderViewHolder(val view: View) : RecyclerView.ViewHolder(view) - class AttachmentViewHolder(val binding: ItemHomeworkDialogAttachmentBinding) : - RecyclerView.ViewHolder(binding.root) + class AttachmentViewHolder(val view: View) : RecyclerView.ViewHolder(view) } 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 7b3b9821a..54cb5c68c 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,18 +5,16 @@ 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 @@ -45,7 +43,7 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_homework, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -55,18 +53,14 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew @SuppressLint("SetTextI18n") override fun initView() { - 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() } - } + 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.homeworkDialogRecycler) { + with(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 } } @@ -74,7 +68,7 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew override fun updateMarkAsDoneLabel(isDone: Boolean) { (parentFragment as? HomeworkFragment)?.onReloadList() - binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) + 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 ffb36fe6e..19f23593e 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,9 +14,10 @@ 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 @@ -29,13 +30,13 @@ class LoginActivity : BaseActivity(), Logi fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) } - override val currentViewIndex get() = binding.loginViewpager.currentItem + override val currentViewIndex get() = loginViewpager.currentItem override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) - setSupportActionBar(binding.loginToolbar) - messageContainer = binding.loginContainer + setContentView(R.layout.activity_login) + setSupportActionBar(loginToolbar) + messageContainer = loginContainer presenter.onAttachView(this) } @@ -47,7 +48,7 @@ class LoginActivity : BaseActivity(), Logi } with(loginAdapter) { - containerId = binding.loginViewpager.id + containerId = loginViewpager.id addFragments(listOf( LoginFormFragment.newInstance(), LoginSymbolFragment.newInstance(), @@ -57,7 +58,7 @@ class LoginActivity : BaseActivity(), Logi )) } - with(binding.loginViewpager) { + with(loginViewpager) { offscreenPageLimit = 2 adapter = loginAdapter setOnSelectPageListener(presenter::onViewSelected) @@ -70,7 +71,7 @@ class LoginActivity : BaseActivity(), Logi } override fun switchView(index: Int) { - binding.loginViewpager.setCurrentItem(index, false) + 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 3b6a985be..aaea31eca 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,14 +1,15 @@ 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 @@ -16,11 +17,10 @@ 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(R.layout.fragment_login_advanced), - LoginAdvancedView { +class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView { @Inject lateinit var presenter: LoginAdvancedPresenter @@ -30,17 +30,17 @@ class LoginAdvancedFragment : } override val formLoginType: String - get() = when (binding.loginTypeSwitch.checkedRadioButtonId) { + get() = when (loginTypeSwitch.checkedRadioButtonId) { R.id.loginTypeApi -> "API" R.id.loginTypeScrapper -> "SCRAPPER" else -> "HYBRID" } override val formUsernameValue: String - get() = binding.loginFormUsername.text.toString().trim() + get() = loginFormUsername.text.toString().trim() override val formPassValue: String - get() = binding.loginFormPass.text.toString().trim() + get() = loginFormPass.text.toString().trim() private lateinit var hostKeys: Array @@ -49,19 +49,19 @@ class LoginAdvancedFragment : private lateinit var hostSymbols: Array override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty() override val formPinValue: String - get() = binding.loginFormPin.text.toString().trim() + get() = loginFormPin.text.toString().trim() override val formSymbolValue: String - get() = binding.loginFormSymbol.text.toString().trim() + get() = loginFormSymbol.text.toString().trim() override val formTokenValue: String - get() = binding.loginFormToken.text.toString().trim() + get() = loginFormToken.text.toString().trim() override val nicknameLabel: String get() = getString(R.string.login_nickname_hint) @@ -69,9 +69,12 @@ class LoginAdvancedFragment : override val emailLabel: String get() = getString(R.string.login_email_hint) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentLoginAdvancedBinding.bind(view) + 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) presenter.onAttachView(this) } @@ -80,201 +83,191 @@ class LoginAdvancedFragment : hostValues = resources.getStringArray(R.array.hosts_values) hostSymbols = resources.getStringArray(R.array.hosts_symbols) - 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() } + 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 - }) - } - - 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))) + loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> + presenter.onLoginModeSelected(when (checkedId) { + R.id.loginTypeApi -> Sdk.Mode.API + R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER + else -> Sdk.Mode.HYBRID + }) } - with(binding.loginFormHost) { + 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) { setText(hostKeys.getOrNull(0).orEmpty()) setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) - setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } + setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() } } } override fun showMobileApiWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api) + loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api) } override fun showScraperWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper) + loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper) } override fun showHybridWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid) + loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid) } override fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String) { - with(binding) { - loginFormUsername.setText(username) - loginFormPass.setText(pass) - loginFormToken.setText(token) - loginFormSymbol.setText(symbol) - loginFormPin.setText(pin) - } + loginFormUsername.setText(username) + loginFormPass.setText(pass) + loginFormToken.setText(token) + loginFormSymbol.setText(symbol) + loginFormPin.setText(pin) } override fun setUsernameLabel(label: String) { - binding.loginFormUsernameLayout.hint = label + loginFormUsernameLayout.hint = label } override fun setSymbol(symbol: String) { - binding.loginFormSymbol.setText(symbol) + loginFormSymbol.setText(symbol) } override fun setErrorUsernameRequired() { - with(binding.loginFormUsernameLayout) { + with(loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorLoginRequired() { - with(binding.loginFormUsernameLayout) { + with(loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_invalid_login) } } override fun setErrorEmailRequired() { - with(binding.loginFormUsernameLayout) { + with(loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_invalid_email) } } override fun setErrorPassRequired(focus: Boolean) { - with(binding.loginFormPassLayout) { + with(loginFormPassLayout) { if (focus) requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorPassInvalid(focus: Boolean) { - with(binding.loginFormPassLayout) { + with(loginFormPassLayout) { if (focus) requestFocus() error = getString(R.string.login_invalid_password) } } override fun setErrorPassIncorrect() { - with(binding.loginFormPassLayout) { + with(loginFormPassLayout) { requestFocus() error = getString(R.string.login_incorrect_password) } } override fun setErrorPinRequired() { - with(binding.loginFormPinLayout) { + with(loginFormPinLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorPinInvalid(message: String) { - with(binding.loginFormPinLayout) { + with(loginFormPinLayout) { requestFocus() error = message } } override fun setErrorSymbolRequired() { - with(binding.loginFormSymbolLayout) { + with(loginFormSymbolLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorSymbolInvalid(message: String) { - with(binding.loginFormSymbolLayout) { + with(loginFormSymbolLayout) { requestFocus() error = message } } override fun setErrorTokenRequired() { - with(binding.loginFormTokenLayout) { + with(loginFormTokenLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorTokenInvalid(message: String) { - with(binding.loginFormTokenLayout) { + with(loginFormTokenLayout) { requestFocus() error = message } } override fun clearUsernameError() { - binding.loginFormUsernameLayout.error = null + loginFormUsernameLayout.error = null } override fun clearPassError() { - binding.loginFormPassLayout.error = null + loginFormPassLayout.error = null } override fun clearPinKeyError() { - binding.loginFormPinLayout.error = null + loginFormPinLayout.error = null } override fun clearSymbolError() { - binding.loginFormSymbolLayout.error = null + loginFormSymbolLayout.error = null } override fun clearTokenError() { - binding.loginFormTokenLayout.error = null + loginFormTokenLayout.error = null } override fun showOnlyHybridModeInputs() { - with(binding) { - loginFormUsernameLayout.visibility = VISIBLE - loginFormPassLayout.visibility = VISIBLE - loginFormHostLayout.visibility = VISIBLE - loginFormPinLayout.visibility = GONE - loginFormSymbolLayout.visibility = VISIBLE - loginFormTokenLayout.visibility = GONE - } + loginFormUsernameLayout.visibility = VISIBLE + loginFormPassLayout.visibility = VISIBLE + loginFormHostLayout.visibility = VISIBLE + loginFormPinLayout.visibility = GONE + loginFormSymbolLayout.visibility = VISIBLE + loginFormTokenLayout.visibility = GONE } override fun showOnlyScrapperModeInputs() { - with(binding) { - loginFormUsernameLayout.visibility = VISIBLE - loginFormPassLayout.visibility = VISIBLE - loginFormHostLayout.visibility = VISIBLE - loginFormPinLayout.visibility = GONE - loginFormSymbolLayout.visibility = VISIBLE - loginFormTokenLayout.visibility = GONE - } + loginFormUsernameLayout.visibility = VISIBLE + loginFormPassLayout.visibility = VISIBLE + loginFormHostLayout.visibility = VISIBLE + loginFormPinLayout.visibility = GONE + loginFormSymbolLayout.visibility = VISIBLE + loginFormTokenLayout.visibility = GONE } override fun showOnlyMobileApiModeInputs() { - with(binding) { - loginFormUsernameLayout.visibility = GONE - loginFormPassLayout.visibility = GONE - loginFormHostLayout.visibility = GONE - loginFormPinLayout.visibility = VISIBLE - loginFormSymbolLayout.visibility = VISIBLE - loginFormTokenLayout.visibility = VISIBLE - } + loginFormUsernameLayout.visibility = GONE + loginFormPassLayout.visibility = GONE + loginFormHostLayout.visibility = GONE + loginFormPinLayout.visibility = VISIBLE + loginFormSymbolLayout.visibility = VISIBLE + loginFormTokenLayout.visibility = VISIBLE } override fun showSoftKeyboard() { @@ -286,17 +279,17 @@ class LoginAdvancedFragment : } override fun showProgress(show: Boolean) { - binding.loginFormProgress.visibility = if (show) VISIBLE else GONE + loginFormProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - binding.loginFormContainer.visibility = if (show) VISIBLE else GONE + loginFormContainer.visibility = if (show) VISIBLE else GONE } override fun notifyParentAccountLogged(students: List) { (activity as? LoginActivity)?.onFormFragmentAccountLogged(students, Triple( - binding.loginFormUsername.text.toString(), - binding.loginFormPass.text.toString(), + loginFormUsername.text.toString(), + loginFormPass.text.toString(), resources.getStringArray(R.array.hosts_values)[1] )) } @@ -307,7 +300,7 @@ class LoginAdvancedFragment : } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } } 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 a2a083c00..058e702e6 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,13 +2,14 @@ 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 @@ -17,10 +18,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(R.layout.fragment_login_form), - LoginFormView { +class LoginFormFragment : BaseFragment(), LoginFormView { @Inject lateinit var presenter: LoginFormPresenter @@ -33,16 +34,16 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override val formUsernameValue: String - get() = binding.loginFormUsername.text.toString() + get() = loginFormUsername.text.toString() override val formPassValue: String - get() = binding.loginFormPass.text.toString() + get() = loginFormPass.text.toString() override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty() override val nicknameLabel: String get() = getString(R.string.login_nickname_hint) @@ -56,9 +57,12 @@ class LoginFormFragment : BaseFragment(R.layout.fragme private lateinit var hostSymbols: Array - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentLoginFormBinding.bind(view) + 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) presenter.onAttachView(this) } @@ -67,85 +71,81 @@ class LoginFormFragment : BaseFragment(R.layout.fragme hostValues = resources.getStringArray(R.array.hosts_values) hostSymbols = resources.getStringArray(R.array.hosts_symbols) - 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() } - } + 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.loginFormHost) { + with(loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) - setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } + setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() } } } override fun setCredentials(username: String, pass: String) { - with(binding) { - loginFormUsername.setText(username) - loginFormPass.setText(pass) - } + loginFormUsername.setText(username) + loginFormPass.setText(pass) } override fun setUsernameLabel(label: String) { - binding.loginFormUsernameLayout.hint = label + loginFormUsernameLayout.hint = label } override fun setErrorUsernameRequired() { - with(binding.loginFormUsernameLayout) { + with(loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorLoginRequired() { - with(binding.loginFormUsernameLayout) { + with(loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_invalid_login) } } override fun setErrorEmailRequired() { - with(binding.loginFormUsernameLayout) { + with(loginFormUsernameLayout) { requestFocus() error = getString(R.string.login_invalid_email) } } override fun setErrorPassRequired(focus: Boolean) { - with(binding.loginFormPassLayout) { + with(loginFormPassLayout) { if (focus) requestFocus() error = getString(R.string.login_field_required) } } override fun setErrorPassInvalid(focus: Boolean) { - with(binding.loginFormPassLayout) { + with(loginFormPassLayout) { if (focus) requestFocus() error = getString(R.string.login_invalid_password) } } override fun setErrorPassIncorrect() { - with(binding.loginFormPassLayout) { + with(loginFormPassLayout) { requestFocus() error = getString(R.string.login_incorrect_password) } } override fun clearUsernameError() { - binding.loginFormUsernameLayout.error = null + loginFormUsernameLayout.error = null } override fun clearPassError() { - binding.loginFormPassLayout.error = null + loginFormPassLayout.error = null } override fun showSoftKeyboard() { @@ -157,16 +157,16 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override fun showProgress(show: Boolean) { - binding.loginFormProgress.visibility = if (show) VISIBLE else GONE + loginFormProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - binding.loginFormContainer.visibility = if (show) VISIBLE else GONE + loginFormContainer.visibility = if (show) VISIBLE else GONE } @SuppressLint("SetTextI18n") override fun showVersion() { - binding.loginFormVersion.text = "v${appInfo.versionName}" + loginFormVersion.text = "v${appInfo.versionName}" } override fun notifyParentAccountLogged(students: List, loginData: Triple) { @@ -178,7 +178,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override fun showContact(show: Boolean) { - binding.loginFormContact.visibility = if (show) VISIBLE else GONE + loginFormContact.visibility = if (show) VISIBLE else GONE } override fun openAdvancedLogin() { @@ -190,8 +190,8 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } 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 e27c845a6..1bff49262 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,28 +3,25 @@ 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(R.layout.fragment_login_recover), LoginRecoverView { - - private var _binding: FragmentLoginRecoverBinding? = null - - private val bindingLocal: FragmentLoginRecoverBinding get() = _binding!! +class LoginRecoverFragment : BaseFragment(), LoginRecoverView { @Inject lateinit var presenter: LoginRecoverPresenter @@ -40,13 +37,13 @@ class LoginRecoverFragment : private lateinit var hostSymbols: Array override val recoverHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(bindingLocal.loginRecoverHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(loginRecoverHost.text.toString())).orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(bindingLocal.loginRecoverHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(loginRecoverHost.text.toString())).orEmpty() override val recoverNameValue: String - get() = bindingLocal.loginRecoverName.text.toString().trim() + get() = loginRecoverName.text.toString().trim() override val emailHintString: String get() = getString(R.string.login_email_hint) @@ -57,90 +54,91 @@ class LoginRecoverFragment : override val invalidEmailString: String get() = getString(R.string.login_invalid_email) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - _binding = FragmentLoginRecoverBinding.bind(view) + 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) 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) - with(bindingLocal) { - 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) } - } + 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(bindingLocal.loginRecoverHost) { + with(loginRecoverHost) { setText(hostKeys.getOrNull(0).orEmpty()) setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) - setOnClickListener { if (bindingLocal.loginRecoverFormContainer.visibility == GONE) dismissDropDown() } + setOnClickListener { if (loginRecoverFormContainer.visibility == GONE) dismissDropDown() } } } override fun setDefaultCredentials(username: String) { - bindingLocal.loginRecoverName.setText(username) + loginRecoverName.setText(username) } override fun setErrorNameRequired() { - with(bindingLocal.loginRecoverNameLayout) { + with(loginRecoverNameLayout) { requestFocus() error = getString(R.string.login_field_required) } } override fun setUsernameHint(hint: String) { - bindingLocal.loginRecoverNameLayout.hint = hint + loginRecoverNameLayout.hint = hint } override fun setUsernameError(message: String) { - with(bindingLocal.loginRecoverNameLayout) { + with(loginRecoverNameLayout) { requestFocus() error = message } } override fun clearUsernameError() { - bindingLocal.loginRecoverNameLayout.error = null + loginRecoverNameLayout.error = null } override fun showProgress(show: Boolean) { - bindingLocal.loginRecoverProgress.visibility = if (show) VISIBLE else GONE + loginRecoverProgress.visibility = if (show) VISIBLE else GONE } override fun showRecoverForm(show: Boolean) { - bindingLocal.loginRecoverFormContainer.visibility = if (show) VISIBLE else GONE + loginRecoverFormContainer.visibility = if (show) VISIBLE else GONE } override fun showCaptcha(show: Boolean) { - bindingLocal.loginRecoverCaptchaContainer.visibility = if (show) VISIBLE else GONE + loginRecoverCaptchaContainer.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - bindingLocal.loginRecoverError.visibility = if (show) VISIBLE else GONE + loginRecoverError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - bindingLocal.loginRecoverErrorMessage.text = message + loginRecoverErrorMessage.text = message } override fun showSuccessView(show: Boolean) { - bindingLocal.loginRecoverSuccess.visibility = if (show) VISIBLE else GONE + loginRecoverSuccess.visibility = if (show) VISIBLE else GONE } override fun setSuccessTitle(title: String) { - bindingLocal.loginRecoverSuccessTitle.text = title + loginRecoverSuccessTitle.text = title } override fun setSuccessMessage(message: String) { - bindingLocal.loginRecoverSuccessMessage.text = message + loginRecoverSuccessMessage.text = message } override fun showSoftKeyboard() { @@ -161,7 +159,7 @@ class LoginRecoverFragment : callback:e =>Android.captchaCallback(e)}) """.trimIndent() - with(bindingLocal.loginRecoverWebView) { + with(loginRecoverWebView) { settings.javaScriptEnabled = true webViewClient = object : WebViewClient() { private var recoverWebViewSuccess: Boolean = true @@ -183,8 +181,6 @@ class LoginRecoverFragment : loadDataWithBaseURL(url, html, "text/html", "UTF-8", null) addJavascriptInterface(object { - - @Suppress("UNUSED") @JavascriptInterface fun captchaCallback(reCaptchaResponse: String) { activity?.runOnUiThread { @@ -201,10 +197,8 @@ class LoginRecoverFragment : } override fun onDestroyView() { - bindingLocal.loginRecoverWebView.destroy() - _binding = null - presenter.onDetachView() - super.onDestroyView() + loginRecoverWebView.destroy() + presenter.onDetachView() } } 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 deleted file mode 100644 index 3332f2be8..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt +++ /dev/null @@ -1,63 +0,0 @@ -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() { - - private val checkedList = mutableMapOf() - - var items = emptyList>() - set(value) { - field = value - checkedList.clear() - } - - 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 - loginItemSignedIn.visibility = if (alreadySaved) View.VISIBLE else View.GONE - - with(loginItemCheck) { - isEnabled = !alreadySaved - keyListener = null - isChecked = checkedList[position] ?: false - } - - root.setOnClickListener { - onClickListener(student, alreadySaved) - - with(loginItemCheck) { - if (isEnabled) { - isChecked = !isChecked - checkedList[position] = 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 b0df51991..9860af22f 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,30 +1,33 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import android.view.ViewGroup +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.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(R.layout.fragment_login_student_select), - LoginStudentSelectView { +class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView { @Inject lateinit var presenter: LoginStudentSelectPresenter @Inject - lateinit var loginAdapter: LoginStudentSelectAdapter + lateinit var loginAdapter: FlexibleAdapter> @Inject lateinit var appInfo: AppInfo @@ -35,32 +38,29 @@ class LoginStudentSelectFragment : fun newInstance() = LoginStudentSelectFragment() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentLoginStudentSelectBinding.bind(view) + 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) presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_STUDENTS)) } override fun initView() { - loginAdapter.onClickListener = presenter::onItemSelected + loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() } + loginAdapter.apply { setOnItemClickListener { presenter.onItemSelected(it) } } + loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() } + loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() } - with(binding) { - loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() } - loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() } - loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() } - - with(loginStudentSelectRecycler) { - layoutManager = LinearLayoutManager(context) - adapter = loginAdapter - } + loginStudentSelectRecycler.apply { + adapter = loginAdapter + layoutManager = SmoothScrollLinearLayoutManager(context) } } - override fun updateData(data: List>) { - with(loginAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + loginAdapter.updateDataSet(data) } override fun openMainView() { @@ -68,15 +68,15 @@ class LoginStudentSelectFragment : } override fun showProgress(show: Boolean) { - binding.loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE + loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - binding.loginStudentSelectContent.visibility = if (show) VISIBLE else GONE + loginStudentSelectContent.visibility = if (show) VISIBLE else GONE } override fun enableSignIn(enable: Boolean) { - binding.loginStudentSelectSignIn.isEnabled = enable + loginStudentSelectSignIn.isEnabled = enable } fun onParentInitStudentSelectFragment(students: List) { @@ -89,7 +89,7 @@ class LoginStudentSelectFragment : } override fun showContact(show: Boolean) { - binding.loginStudentSelectContact.visibility = if (show) VISIBLE else GONE + 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 new file mode 100644 index 000000000..06be61387 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectItem.kt @@ -0,0 +1,71 @@ +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 db25c0da3..82de5a887 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,5 +1,6 @@ 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 @@ -22,7 +23,7 @@ class LoginStudentSelectPresenter @Inject constructor( var students = emptyList() - private val selectedStudents = mutableListOf() + private var selectedStudents = mutableListOf() fun onAttachView(view: LoginStudentSelectView, students: Serializable?) { super.onAttachView(view) @@ -50,14 +51,13 @@ class LoginStudentSelectPresenter @Inject constructor( if (students.size == 1) registerStudents(students) } - fun onItemSelected(student: Student, alreadySaved: Boolean) { - if (alreadySaved) return + fun onItemSelected(item: AbstractFlexibleItem<*>?) { + if (item is LoginStudentSelectItem && !item.alreadySaved) { + selectedStudents.removeAll { it == item.student } + .let { if (!it) selectedStudents.add(item.student) } - selectedStudents - .removeAll { it == student } - .let { if (!it) selectedStudents.add(student) } - - view?.enableSignIn(selectedStudents.isNotEmpty()) + view?.enableSignIn(selectedStudents.isNotEmpty()) + } } private fun compareStudents(a: Student, b: Student): Boolean { @@ -69,31 +69,27 @@ class LoginStudentSelectPresenter @Inject constructor( } private fun loadData(students: List) { - resetSelectedState() this.students = students disposable.add(studentRepository.getSavedStudents() .map { savedStudents -> students.map { student -> - student to savedStudents.any { compareStudents(student, it) } + Pair(student, savedStudents.any { compareStudents(student, it) }) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .subscribe({ - view?.updateData(it) + view?.updateData(it.map { studentPair -> + LoginStudentSelectItem(studentPair.first, studentPair.second) + }) }, { errorHandler.dispatch(it) lastError = it - view?.updateData(students.map { student -> student to false }) + view?.updateData(students.map { student -> LoginStudentSelectItem(student, false) }) }) ) } - private fun resetSelectedState() { - selectedStudents.clear() - view?.enableSignIn(false) - } - private fun registerStudents(students: List) { disposable.add(studentRepository.saveStudents(students) .map { students.first().apply { id = it.first() } } 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 89431dfc6..d80b059db 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,13 +1,12 @@ 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 befbffd50..5602b6248 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,16 +1,17 @@ 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 @@ -18,10 +19,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(R.layout.fragment_login_symbol), LoginSymbolView { +class LoginSymbolFragment : BaseFragment(), LoginSymbolView { @Inject lateinit var presenter: LoginSymbolPresenter @@ -36,28 +37,29 @@ class LoginSymbolFragment : } override val symbolNameError: CharSequence? - get() = binding.loginSymbolNameLayout.error + get() = loginSymbolNameLayout.error - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentLoginSymbolBinding.bind(view) + 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) presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_LOGIN_DATA)) } override fun initView() { - with(binding) { - loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) } - loginSymbolFaq.setOnClickListener { presenter.onFaqClick() } - loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() } + 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 - } - setAdapter(ArrayAdapter(context, android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) + 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))) } } @@ -66,25 +68,25 @@ class LoginSymbolFragment : } override fun setErrorSymbolIncorrect() { - binding.loginSymbolNameLayout.apply { + loginSymbolNameLayout.apply { requestFocus() error = getString(R.string.login_incorrect_symbol) } } override fun setErrorSymbolRequire() { - binding.loginSymbolNameLayout.apply { + loginSymbolNameLayout.apply { requestFocus() error = getString(R.string.login_field_required) } } override fun clearSymbolError() { - binding.loginSymbolNameLayout.error = null + loginSymbolNameLayout.error = null } override fun clearAndFocusSymbol() { - binding.loginSymbolNameLayout.apply { + loginSymbolNameLayout.apply { editText?.text = null requestFocus() } @@ -99,11 +101,11 @@ class LoginSymbolFragment : } override fun showProgress(show: Boolean) { - binding.loginSymbolProgress.visibility = if (show) VISIBLE else GONE + loginSymbolProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE + loginSymbolContainer.visibility = if (show) VISIBLE else GONE } override fun notifyParentAccountLogged(students: List) { @@ -116,12 +118,12 @@ class LoginSymbolFragment : } override fun showContact(show: Boolean) { - binding.loginSymbolContact.visibility = if (show) VISIBLE else GONE + loginSymbolContact.visibility = if (show) VISIBLE else GONE } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } override fun openFaqPage() { @@ -137,7 +139,7 @@ class LoginSymbolFragment : "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName, - "$host/${binding.loginSymbolName.text}", + "$host/${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 0775ce189..12bf1a132 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(R.layout.fragment_lucky_number), LuckyNumberView, - MainView.TitledView { +class LuckyNumberFragment : BaseFragment(), LuckyNumberView, MainView.TitledView { @Inject lateinit var presenter: LuckyNumberPresenter @@ -25,57 +25,58 @@ class LuckyNumberFragment : override val titleStringId: Int get() = R.string.lucky_number_title - override val isViewEmpty get() = binding.luckyNumberText.text.isBlank() + override val isViewEmpty get() = luckyNumberText.text.isBlank() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentLuckyNumberBinding.bind(view) - messageContainer = binding.luckyNumberSwipe + 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 presenter.onAttachView(this) } override fun initView() { - with(binding) { - luckyNumberSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() } - luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() } - } + luckyNumberSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() } + luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() } } override fun updateData(data: LuckyNumber) { - binding.luckyNumberText.text = data.luckyNumber.toString() + luckyNumberText.text = data.luckyNumber.toString() } override fun hideRefresh() { - binding.luckyNumberSwipe.isRefreshing = false + luckyNumberSwipe.isRefreshing = false } override fun showEmpty(show: Boolean) { - binding.luckyNumberEmpty.visibility = if (show) VISIBLE else GONE + luckyNumberEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.luckyNumberError.visibility = if (show) VISIBLE else GONE + luckyNumberError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.luckyNumberErrorMessage.text = message + luckyNumberErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.luckyNumberProgress.visibility = if (show) VISIBLE else GONE + luckyNumberProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.luckyNumberSwipe.isEnabled = enable + luckyNumberSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.luckyNumberContent.visibility = if (show) VISIBLE else GONE + luckyNumberContent.visibility = if (show) VISIBLE else GONE } override fun onDestroyView() { - presenter.onDetachView() super.onDestroyView() + presenter.onDetachView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt index e932fedc0..1d3a17bdc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt @@ -54,12 +54,7 @@ class LuckyNumberPresenter @Inject constructor( showEmpty(false) showErrorView(false) } - analytics.logEvent( - "load_item", - "type" to "lucky_number", - "number" to it.luckyNumber, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_lucky_number", "lucky_number" to it.luckyNumber, "force_refresh" to forceRefresh) }, { Timber.i("Loading lucky number result: An exception occurred") errorHandler.dispatch(it) 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 961ac6338..e8ce3bcfb 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,39 +4,34 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.R -import io.github.wulkanowy.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.AppInfo +import io.github.wulkanowy.utils.setOnItemClickListener +import kotlinx.android.synthetic.main.activity_widget_configure.* import javax.inject.Inject -class LuckyNumberWidgetConfigureActivity : - BaseActivity(), +class LuckyNumberWidgetConfigureActivity : BaseActivity(), LuckyNumberWidgetConfigureView { @Inject - lateinit var configureAdapter: WidgetConfigureAdapter + lateinit var configureAdapter: FlexibleAdapter> @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(ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root) + setContentView(R.layout.activity_widget_configure) intent.extras.let { presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID)) @@ -44,35 +39,31 @@ class LuckyNumberWidgetConfigureActivity : } override fun initView() { - with(binding.widgetConfigureRecycler) { + with(widgetConfigureRecycler) { adapter = configureAdapter - layoutManager = LinearLayoutManager(context) + layoutManager = SmoothScrollLinearLayoutManager(context) } - configureAdapter.onClickListener = presenter::onItemSelect + configureAdapter.setOnItemClickListener(presenter::onItemSelect) } override fun showThemeDialog() { - var items = arrayOf( + val items = arrayOf( getString(R.string.widget_timetable_theme_light), getString(R.string.widget_timetable_theme_dark) ) - if (appInfo.systemVersion >= 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>) { - with(configureAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + configureAdapter.updateDataSet(data) } 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 new file mode 100644 index 000000000..e260b7fcb --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureItem.kt @@ -0,0 +1,60 @@ +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 468f9b575..6e4716bfa 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,5 +1,6 @@ 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 @@ -28,9 +29,11 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( loadData() } - fun onItemSelect(student: Student) { - selectedStudent = student - view?.showThemeDialog() + fun onItemSelect(item: AbstractFlexibleItem<*>) { + if (item is LuckyNumberWidgetConfigureItem) { + selectedStudent = item.student + view?.showThemeDialog() + } } fun onThemeSelect(index: Int) { @@ -40,7 +43,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( registerStudent(selectedStudent) } - fun onDismissThemeView() { + fun onDismissThemeView(){ view?.finishView() } @@ -48,7 +51,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 -> student to (student.id == currentStudentId) } + students.map { student -> LuckyNumberWidgetConfigureItem(student, student.id == currentStudentId) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) @@ -56,7 +59,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( when { it.isEmpty() -> view?.openLoginView() it.size == 1 -> { - selectedStudent = it.single().first + selectedStudent = it.single().student 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 c8c348ed3..fa4c0cc61 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,6 +1,5 @@ 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 { @@ -9,7 +8,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 55a048b32..e4b1072b6 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,7 +8,6 @@ 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 @@ -63,12 +62,14 @@ 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, getCorrectLayoutId(appWidgetId, context)).apply { + val remoteView = RemoteViews(context.packageName, layoutId).apply { setTextViewText(R.id.luckyNumberWidgetNumber, luckyNumber?.luckyNumber?.toString() ?: "#") setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) } @@ -81,19 +82,17 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { override fun onDeleted(context: Context?, appWidgetIds: IntArray?) { super.onDeleted(context, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> - with(sharedPref) { - delete(getHeightWidgetKey(appWidgetId)) - delete(getStudentWidgetKey(appWidgetId)) - delete(getThemeWidgetKey(appWidgetId)) - delete(getWidthWidgetKey(appWidgetId)) - } + if (appWidgetId != 0) sharedPref.delete(getStudentWidgetKey(appWidgetId)) } } override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) - val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) + 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) setStyles(remoteView, appWidgetId, newOptions) appWidgetManager.updateAppWidget(appWidgetId, remoteView) @@ -103,10 +102,8 @@ 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() - with(sharedPref) { - putLong(getWidthWidgetKey(appWidgetId), width.toLong()) - putLong(getHeightWidgetKey(appWidgetId), height.toLong()) - } + sharedPref.putLong(getWidthWidgetKey(appWidgetId), width.toLong()) + sharedPref.putLong(getHeightWidgetKey(appWidgetId), height.toLong()) val rows = getCellsForSize(height) val cols = getCellsForSize(width) @@ -165,15 +162,4 @@ 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 f5b7c47c8..864ad4239 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,6 +9,7 @@ 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 @@ -19,7 +20,6 @@ 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 @@ -31,15 +31,15 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.more.MoreFragment import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment -import io.github.wulkanowy.utils.FirebaseAnalyticsHelper 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 @@ -47,9 +47,6 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var navController: FragNavController - @Inject - lateinit var analytics: FirebaseAnalyticsHelper - @Inject lateinit var overlayProvider: Lazy @@ -86,9 +83,9 @@ class MainActivity : BaseActivity(), MainVie override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) - setSupportActionBar(binding.mainToolbar) - messageContainer = binding.mainFragmentContainer + setContentView(R.layout.activity_main) + setSupportActionBar(mainToolbar) + messageContainer = mainFragmentContainer presenter.onAttachView(this, intent.getSerializableExtra(EXTRA_START_MENU) as? MainView.Section) @@ -104,12 +101,12 @@ class MainActivity : BaseActivity(), MainVie } override fun initView() { - with(binding.mainToolbar) { + with(mainToolbar) { if (SDK_INT >= LOLLIPOP) stateListAnimator = null setBackgroundColor(overlayProvider.get().compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f))) } - with(binding.mainBottomNav) { + with(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), @@ -118,7 +115,7 @@ class MainActivity : BaseActivity(), MainVie AHBottomNavigationItem(R.string.more_title, R.drawable.ic_main_more, 0) )) accentColor = getThemeAttrColor(R.attr.colorPrimary) - inactiveColor = getThemeAttrColor(R.attr.colorOnSurface, 153) + inactiveColor = ColorUtils.setAlphaComponent(getThemeAttrColor(R.attr.colorOnSurface), 153) defaultBackgroundColor = overlayProvider.get().compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(8f)) titleState = ALWAYS_SHOW currentItem = startMenuIndex @@ -140,10 +137,6 @@ class MainActivity : BaseActivity(), MainVie } } - override fun setCurrentScreen(name: String?) { - analytics.setCurrentScreen(this, name) - } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { return if (item?.itemId == R.id.mainMenuAccount) presenter.onAccountManagerSelected() else false @@ -174,7 +167,7 @@ class MainActivity : BaseActivity(), MainVie } override fun showActionBarElevation(show: Boolean) { - ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) + ViewCompat.setElevation(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 03f6ac382..1610d0298 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,6 +14,7 @@ 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 @@ -25,6 +26,7 @@ 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 @@ -50,7 +52,7 @@ abstract class MainModule { } @PerFragment - @ContributesAndroidInjector + @ContributesAndroidInjector(modules = [AttendanceModule::class]) abstract fun bindAttendanceFragment(): AttendanceFragment @PerFragment @@ -114,7 +116,7 @@ abstract class MainModule { abstract fun bindAccountDialog(): AccountDialog @PerFragment - @ContributesAndroidInjector + @ContributesAndroidInjector(modules = [MobileDeviceModule::class]) abstract fun bindMobileDevices(): MobileDeviceFragment @PerFragment diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 233d44917..f5a490044 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -37,9 +37,8 @@ class MainPresenter @Inject constructor( analytics.logEvent("app_open", "destination" to initMenu?.name) } - fun onViewChange(section: MainView.Section?, name: String?) { + fun onViewChange(section: MainView.Section?) { view?.apply { - setCurrentScreen(name) showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) currentViewTitle?.let { setViewTitle(it) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 7e5831471..97b556e3e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -24,8 +24,6 @@ interface MainView : BaseView { fun showAccountPicker() - fun setCurrentScreen(name: String?) - fun showActionBarElevation(show: Boolean) fun notifyMenuViewReselected() 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 4a9d217b0..7a3e135dd 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,15 +1,16 @@ 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 @@ -17,10 +18,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(R.layout.fragment_message), - MessageView, MainView.TitledView { +class MessageFragment : BaseFragment(), MessageView, MainView.TitledView { @Inject lateinit var presenter: MessagePresenter @@ -34,17 +35,20 @@ class MessageFragment : BaseFragment(R.layout.fragment_m override val titleStringId get() = R.string.message_title - override val currentPageIndex get() = binding.messageViewPager.currentItem + override val currentPageIndex get() = messageViewPager.currentItem - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentMessageBinding.bind(view) + 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) presenter.onAttachView(this) } override fun initView() { with(pagerAdapter) { - containerId = binding.messageViewPager.id + containerId = messageViewPager.id addFragmentsWithTitle(mapOf( MessageTabFragment.newInstance(RECEIVED) to getString(R.string.message_inbox), MessageTabFragment.newInstance(SENT) to getString(R.string.message_sent), @@ -52,29 +56,27 @@ class MessageFragment : BaseFragment(R.layout.fragment_m )) } - with(binding.messageViewPager) { + with(messageViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } - with(binding.messageTabLayout) { - setupWithViewPager(binding.messageViewPager) + with(messageTabLayout) { + setupWithViewPager(messageViewPager) setElevationCompat(context.dpToPx(4f)) } - binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } + openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } } override fun showContent(show: Boolean) { - with(binding) { - messageViewPager.visibility = if (show) VISIBLE else INVISIBLE - messageTabLayout.visibility = if (show) VISIBLE else INVISIBLE - } + messageViewPager.visibility = if (show) VISIBLE else INVISIBLE + messageTabLayout.visibility = if (show) VISIBLE else INVISIBLE } override fun showProgress(show: Boolean) { - binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE + 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 new file mode 100644 index 000000000..7e52233c9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageItem.kt @@ -0,0 +1,67 @@ +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 a94d2cfc8..a0ac6ec70 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,11 +10,10 @@ 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() : @@ -46,47 +45,44 @@ class MessagePreviewAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - 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)) + 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)) else -> throw IllegalStateException() } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is MessageViewHolder -> bindMessage(holder, requireNotNull(messageWithAttachment).message) - is AttachmentViewHolder -> bindAttachment(holder, requireNotNull(messageWithAttachment).attachments[position - 2]) + is MessageViewHolder -> bindMessage(holder.view, requireNotNull(messageWithAttachment).message) + is AttachmentViewHolder -> bindAttachment(holder.view, requireNotNull(messageWithAttachment).attachments[position - 2]) } } @SuppressLint("SetTextI18n") - private fun bindMessage(holder: MessageViewHolder, message: Message) { - with(holder.binding) { - messagePreviewSubject.text = message.subject.ifBlank { 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")) + 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")) messagePreviewContent.text = message.content - 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}" + 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}" } } - private fun bindAttachment(holder: AttachmentViewHolder, attachment: MessageAttachment) { - with(holder.binding) { + private fun bindAttachment(view: View, attachment: MessageAttachment) { + with(view) { messagePreviewAttachment.visibility = View.VISIBLE messagePreviewAttachment.text = attachment.filename - root.setOnClickListener { - root.context.openInternetBrowser(attachment.url) { } + setOnClickListener { + context.openInternetBrowser(attachment.url) { } } } } - class MessageViewHolder(val binding: ItemMessagePreviewBinding) : - RecyclerView.ViewHolder(binding.root) + class MessageViewHolder(val view: View) : RecyclerView.ViewHolder(view) - class DividerViewHolder(val binding: ItemMessageDividerBinding) : - RecyclerView.ViewHolder(binding.root) + class DividerViewHolder(val view: View) : RecyclerView.ViewHolder(view) - class AttachmentViewHolder(val binding: ItemMessageAttachmentBinding) : - RecyclerView.ViewHolder(binding.root) + class AttachmentViewHolder(val view: View) : RecyclerView.ViewHolder(view) } 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 575db75b9..953238c08 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,37 +1,27 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build import android.os.Bundle -import android.print.PrintAttributes -import android.print.PrintManager +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.webkit.WebResourceRequest -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.annotation.RequiresApi -import androidx.core.content.getSystemService +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 io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.shareText +import kotlinx.android.synthetic.main.fragment_message_preview.* import javax.inject.Inject -class MessagePreviewFragment : - BaseFragment(R.layout.fragment_message_preview), - MessagePreviewView, MainView.TitledView { +class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.TitledView { @Inject lateinit var presenter: MessagePreviewPresenter @@ -39,31 +29,18 @@ class MessagePreviewFragment : @Inject lateinit var previewAdapter: MessagePreviewAdapter - @Inject - lateinit var appInfo: AppInfo - private var menuReplyButton: MenuItem? = null private var menuForwardButton: MenuItem? = null private var menuDeleteButton: MenuItem? = null - private var menuShareButton: MenuItem? = null - - private var menuPrintButton: MenuItem? = null - override val titleStringId: Int get() = R.string.message_title override val deleteMessageSuccessString: String get() = getString(R.string.message_delete_success) - override val messageNoSubjectString: String - get() = getString(R.string.message_no_subject) - - override val printHTML: String - get() = requireContext().assets.open("message-print-page.html").bufferedReader().use { it.readText() } - companion object { const val MESSAGE_ID_KEY = "message_id" @@ -79,17 +56,20 @@ class MessagePreviewFragment : setHasOptionsMenu(true) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentMessagePreviewBinding.bind(view) - messageContainer = binding.messagePreviewContainer + 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 presenter.onAttachView(this, (savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message) } override fun initView() { - binding.messagePreviewErrorDetails.setOnClickListener { presenter.onDetailsClick() } + messagePreviewErrorDetails.setOnClickListener { presenter.onDetailsClick() } - with(binding.messagePreviewRecycler) { + with(messagePreviewRecycler) { layoutManager = LinearLayoutManager(context) adapter = previewAdapter } @@ -100,8 +80,6 @@ class MessagePreviewFragment : menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply) menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward) menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete) - menuShareButton = menu.findItem(R.id.messagePreviewMenuShare) - menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint) presenter.onCreateOptionsMenu() } @@ -110,8 +88,6 @@ class MessagePreviewFragment : R.id.messagePreviewMenuReply -> presenter.onReply() R.id.messagePreviewMenuForward -> presenter.onForward() R.id.messagePreviewMenuDelete -> presenter.onMessageDelete() - R.id.messagePreviewMenuShare -> presenter.onShare() - R.id.messagePreviewMenuPrint -> presenter.onPrint() else -> false } } @@ -124,19 +100,17 @@ class MessagePreviewFragment : } override fun showProgress(show: Boolean) { - binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE + messagePreviewProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE + messagePreviewRecycler.visibility = if (show) VISIBLE else GONE } override fun showOptions(show: Boolean) { menuReplyButton?.isVisible = show menuForwardButton?.isVisible = show menuDeleteButton?.isVisible = show - menuShareButton?.isVisible = show - menuPrintButton?.isVisible = show && appInfo.systemVersion >= Build.VERSION_CODES.LOLLIPOP } override fun setDeletedOptionsLabels() { @@ -148,15 +122,15 @@ class MessagePreviewFragment : } override fun showErrorView(show: Boolean) { - binding.messagePreviewError.visibility = if (show) VISIBLE else GONE + messagePreviewError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.messagePreviewErrorMessage.text = message + messagePreviewErrorMessage.text = message } override fun setErrorRetryCallback(callback: () -> Unit) { - binding.messagePreviewErrorRetry.setOnClickListener { callback() } + messagePreviewErrorRetry.setOnClickListener { callback() } } override fun openMessageReply(message: Message?) { @@ -167,38 +141,6 @@ class MessagePreviewFragment : context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message)) } } - override fun shareText(text: String, subject: String) { - context?.shareText(text, subject) - } - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - override fun printDocument(html: String, jobName: String) { - val webView = WebView(activity) - webView.webViewClient = object : WebViewClient() { - - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false - - override fun onPageFinished(view: WebView, url: String) { - createWebPrintJob(view, jobName) - } - } - - webView.loadDataWithBaseURL("file:///android_asset/", html, "text/HTML", "UTF-8", null) - } - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private fun createWebPrintJob(webView: WebView, jobName: String) { - activity?.getSystemService()?.let { printManager -> - val printAdapter = webView.createPrintDocumentAdapter(jobName) - - printManager.print( - jobName, - printAdapter, - PrintAttributes.Builder().build() - ) - } - } - override fun popView() { (activity as MainActivity).popView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index db7996bca..62ac5a532 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -1,17 +1,12 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.annotation.SuppressLint -import android.os.Build import io.github.wulkanowy.data.db.entities.Message -import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.repositories.message.MessageRepository import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.SchedulersProvider -import io.github.wulkanowy.utils.toFormattedString import timber.log.Timber import javax.inject.Inject @@ -20,14 +15,11 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val analytics: FirebaseAnalyticsHelper, - private var appInfo: AppInfo + private val analytics: FirebaseAnalyticsHelper ) : BasePresenter(errorHandler, studentRepository, schedulers) { var message: Message? = null - var attachments: List? = null - private lateinit var lastError: Throwable private var retryCallback: () -> Unit = {} @@ -64,16 +56,11 @@ class MessagePreviewPresenter @Inject constructor( .subscribe({ message -> Timber.i("Loading message ${message.message.messageId} preview result: Success ") this@MessagePreviewPresenter.message = message.message - this@MessagePreviewPresenter.attachments = message.attachments view?.apply { setMessageWithAttachment(message) initOptions() } - analytics.logEvent( - "load_item", - "type" to "message_preview", - "length" to message.message.content.length - ) + analytics.logEvent("load_message_preview", "length" to message.message.content.length) }) { Timber.i("Loading message ${message.messageId} preview result: An exception occurred ") retryCallback = { onMessageLoadRetry(message) } @@ -96,60 +83,6 @@ class MessagePreviewPresenter @Inject constructor( } else false } - fun onShare(): Boolean { - message?.let { - var text = "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { - true -> "Od: ${it.sender}\n" - false -> "Do: ${it.recipient}\n" - } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" - - attachments?.let { attachments -> - if (attachments.isNotEmpty()) { - text += "\n\nZałączniki:" - - attachments.forEach { attachment -> - text += "\n${attachment.filename}: ${attachment.url}" - } - } - } - - view?.shareText(text, "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}") - return true - } - return false - } - - @SuppressLint("NewApi") - fun onPrint(): Boolean { - if (appInfo.systemVersion < Build.VERSION_CODES.LOLLIPOP) return false - message?.let { - val dateString = it.date.toFormattedString("yyyy-MM-dd HH:mm:ss") - val infoContent = "

Data wysłania

$dateString
" + when { - it.sender.isNotEmpty() -> "

Od

${it.sender}
" - else -> "

Do

${it.recipient}
" - } - - val messageContent = "

${it.content}

" - .replace(Regex("[\\n\\r]{2,}"), "

") - .replace(Regex("[\\n\\r]"), "
") - - val jobName = "Wiadomość " + when { - it.sender.isNotEmpty() -> "od ${it.sender}" - else -> "do ${it.recipient}" - } + " $dateString: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }} | Wulkanowy" - - view?.apply { - val html = printHTML - .replace("%SUBJECT%", it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) - .replace("%CONTENT%", messageContent) - .replace("%INFO%", infoContent) - printDocument(html, jobName) - } - return true - } - return false - } - private fun deleteMessage() { message?.let { message -> disposable.add(studentRepository.getCurrentStudent() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt index 0fdb4bda3..3d620459c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -1,7 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build -import androidx.annotation.RequiresApi import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.ui.base.BaseView @@ -10,10 +8,6 @@ interface MessagePreviewView : BaseView { val deleteMessageSuccessString: String - val messageNoSubjectString: String - - val printHTML: String - fun initView() fun setMessageWithAttachment(item: MessageWithAttachment) @@ -40,10 +34,5 @@ interface MessagePreviewView : BaseView { fun openMessageForward(message: Message?) - fun shareText(text: String, subject: String) - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - fun printDocument(html: String, jobName: String) - fun popView() } 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 7b7503433..232b05d79 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 - get() = binding.sendMessageTo.addedChipItems as List + get() = sendMessageTo.addedChipItems as List override val formSubjectValue: String - get() = binding.sendMessageSubject.text.toString() + get() = sendMessageSubject.text.toString() override val formContentValue: String - get() = binding.sendMessageMessageContent.text.toString() + get() = sendMessageMessageContent.text.toString() override val messageRequiredRecipients: String get() = getString(R.string.message_required_recipients) @@ -64,20 +64,18 @@ class SendMessageActivity : BaseActivity presenter.onTouchScroll() } - sendMessageTo.onTextChangeListener = presenter::onRecipientsTextChange - } + sendMessageScroll.setOnTouchListener { _, _ -> presenter.onTouchScroll() } + sendMessageTo.onTextChangeListener = presenter::onRecipientsTextChange } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -95,27 +93,27 @@ class SendMessageActivity : BaseActivity) { - binding.sendMessageTo.filterableChipItems = recipients + sendMessageTo.filterableChipItems = recipients } override fun setSelectedRecipients(recipients: List) { - binding.sendMessageTo.addChips(recipients) + sendMessageTo.addChips(recipients) } override fun showProgress(show: Boolean) { - binding.sendMessageProgress.visibility = if (show) VISIBLE else GONE + sendMessageProgress.visibility = if (show) VISIBLE else GONE } override fun showContent(show: Boolean) { - binding.sendMessageContent.visibility = if (show) VISIBLE else GONE + sendMessageContent.visibility = if (show) VISIBLE else GONE } override fun showEmpty(show: Boolean) { - binding.sendMessageEmpty.visibility = if (show) VISIBLE else GONE + sendMessageEmpty.visibility = if (show) VISIBLE else GONE } override fun showActionBar(show: Boolean) { @@ -123,11 +121,11 @@ class SendMessageActivity : BaseActivity extendHitArea() } + sendMessageMessageContent.post { + sendMessageMessageContent.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> 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 deleted file mode 100644 index b58508a98..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt +++ /dev/null @@ -1,86 +0,0 @@ -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.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.NO_POSITION -import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.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 var items = mutableListOf() - - fun setDataItems(data: List) { - val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data)) - items = data.toMutableList() - diffResult.dispatchUpdatesTo(this) - } - - fun updateItem(position: Int, item: Message) { - val currentItem = items[position] - items[position] = item - if (item != currentItem) { - notifyItemChanged(position) - } - } - - 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 { - holder.adapterPosition.let { if (it != NO_POSITION) onClickListener(item, it) } - } - } - } - - class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root) - - private class MessageTabDiffUtil(private val old: List, private val new: List) : - DiffUtil.Callback() { - override fun getOldListSize(): Int = old.size - - override fun getNewListSize(): Int = new.size - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return old[oldItemPosition].id == new[newItemPosition].id - } - - 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/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index 9954c6428..ce0fc7805 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,33 +1,35 @@ package io.github.wulkanowy.ui.modules.message.tab import android.os.Bundle -import android.view.Menu -import android.view.MenuInflater +import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import androidx.appcompat.widget.SearchView -import androidx.recyclerview.widget.LinearLayoutManager +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 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.ui.widgets.DividerItemDecoration +import io.github.wulkanowy.utils.setOnItemClickListener +import kotlinx.android.synthetic.main.fragment_message_tab.* import javax.inject.Inject -class MessageTabFragment : BaseFragment(R.layout.fragment_message_tab), - MessageTabView { +class MessageTabFragment : BaseFragment(), MessageTabView { @Inject lateinit var presenter: MessageTabPresenter @Inject - lateinit var tabAdapter: MessageTabAdapter + lateinit var tabAdapter: FlexibleAdapter> companion object { const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id" @@ -41,92 +43,78 @@ class MessageTabFragment : BaseFragment(R.layout.frag } } - override val isViewEmpty - get() = tabAdapter.itemCount == 0 + override val noSubjectString: String + get() = getString(R.string.message_no_subject) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) + override val isViewEmpty + get() = tabAdapter.isEmpty + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_message_tab, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentMessageTabBinding.bind(view) - messageContainer = binding.messageTabRecycler + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + messageContainer = messageTabRecycler presenter.onAttachView(this, MessageFolder.valueOf( (savedInstanceState ?: arguments)?.getString(MESSAGE_TAB_FOLDER_ID).orEmpty() )) } override fun initView() { - tabAdapter.onClickListener = presenter::onMessageItemSelected + tabAdapter.setOnItemClickListener { presenter.onMessageItemSelected(it) } - with(binding.messageTabRecycler) { - layoutManager = LinearLayoutManager(context) + messageTabRecycler.run { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = tabAdapter - addItemDecoration(DividerItemDecoration(context)) - } - with(binding) { - messageTabSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - messageTabErrorRetry.setOnClickListener { presenter.onRetry() } - messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider() + .withDrawDividerOnLastItem(false) + ) } + messageTabSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + messageTabErrorRetry.setOnClickListener { presenter.onRetry() } + messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - 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 updateData(data: List) { + tabAdapter.updateDataSet(data) } - override fun updateData(data: List) { - tabAdapter.setDataItems(data) + override fun updateItem(item: AbstractFlexibleItem<*>) { + tabAdapter.updateItem(item) } - override fun updateItem(item: Message, position: Int) { - tabAdapter.updateItem(position, item) + override fun clearView() { + tabAdapter.clear() } override fun showProgress(show: Boolean) { - binding.messageTabProgress.visibility = if (show) VISIBLE else GONE + messageTabProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.messageTabSwipe.isEnabled = enable - } - - override fun resetListPosition() { - binding.messageTabRecycler.scrollToPosition(0) + messageTabSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.messageTabRecycler.visibility = if (show) VISIBLE else INVISIBLE + messageTabRecycler.visibility = if (show) VISIBLE else INVISIBLE } override fun showEmpty(show: Boolean) { - binding.messageTabEmpty.visibility = if (show) VISIBLE else INVISIBLE + messageTabEmpty.visibility = if (show) VISIBLE else INVISIBLE } override fun showErrorView(show: Boolean) { - binding.messageTabError.visibility = if (show) VISIBLE else GONE + messageTabError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.messageTabErrorMessage.text = message + messageTabErrorMessage.text = message } override fun showRefresh(show: Boolean) { - binding.messageTabSwipe.isRefreshing = show + 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 533f5ac85..cbdb89b2e 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,22 +1,17 @@ package io.github.wulkanowy.ui.modules.message.tab -import io.github.wulkanowy.data.db.entities.Message +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 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 io.reactivex.subjects.PublishSubject -import me.xdrop.fuzzywuzzy.FuzzySearch import timber.log.Timber -import java.util.Locale -import java.util.concurrent.TimeUnit import javax.inject.Inject -import kotlin.math.pow class MessageTabPresenter @Inject constructor( schedulers: SchedulersProvider, @@ -31,16 +26,9 @@ class MessageTabPresenter @Inject constructor( private lateinit var lastError: Throwable - private var lastSearchQuery = "" - - private var messages = emptyList() - - private val searchQuery = PublishSubject.create() - fun onAttachView(view: MessageTabView, folder: MessageFolder) { super.onAttachView(view) view.initView() - initializeSearchStream() errorHandler.showErrorMessage = ::showErrorViewOnError this.folder = folder } @@ -70,48 +58,53 @@ class MessageTabPresenter @Inject constructor( loadData(forceRefresh) } - fun onMessageItemSelected(message: Message, position: Int) { - Timber.i("Select message ${message.id} item (position: $position)") - view?.run { - openMessage(message) - if (message.unread) { - message.unread = false - updateItem(message, position) + 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) + } } } } private fun loadData(forceRefresh: Boolean) { Timber.i("Loading $folder message data started") - disposable.add(studentRepository.getCurrentStudent() - .flatMap { student -> - semesterRepository.getCurrentSemester(student) - .flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) } - } - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .doFinally { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) - notifyParentDataLoaded() + disposable.apply { + clear() + add(studentRepository.getCurrentStudent() + .flatMap { student -> + semesterRepository.getCurrentSemester(student) + .flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) } + .map { items -> items.map { MessageItem(it, view?.noSubjectString.orEmpty()) } } } - } - .subscribe({ - Timber.i("Loading $folder message result: Success") - messages = it - view?.updateData(getFilteredData(lastSearchQuery)) - analytics.logEvent( - "load_data", - "type" to "messages", - "items" to it.size, - "folder" to folder.name - ) - }) { - Timber.i("Loading $folder message result: An exception occurred") - errorHandler.dispatch(it) - }) + .subscribeOn(schedulers.backgroundThread) + .observeOn(schedulers.mainThread) + .doFinally { + view?.run { + showRefresh(false) + showProgress(false) + enableSwipe(true) + notifyParentDataLoaded() + } + } + .subscribe({ + Timber.i("Loading $folder message result: Success") + view?.run { + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + showErrorView(false) + updateData(it) + } + analytics.logEvent("load_messages", "items" to it.size, "folder" to folder.name) + }) { + Timber.i("Loading $folder message result: An exception occurred") + errorHandler.dispatch(it) + }) + } } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -124,84 +117,4 @@ class MessageTabPresenter @Inject constructor( } else showError(message, error) } } - - fun onSearchQueryTextChange(query: String) { - if (query != searchQuery.toString()) - searchQuery.onNext(query) - } - - private fun initializeSearchStream() { - disposable.add(searchQuery - .debounce(250, TimeUnit.MILLISECONDS) - .map { query -> - lastSearchQuery = query - getFilteredData(query) - } - .subscribeOn(schedulers.backgroundThread) - .observeOn(schedulers.mainThread) - .subscribe({ - Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${it.size}") - updateData(it) - }) { Timber.e(it) }) - } - - private fun getFilteredData(query: String): List { - return if (query.trim().isEmpty()) { - messages.sortedByDescending { it.date } - } else { - messages - .map { it to calculateMatchRatio(it, query) } - .sortedByDescending { it.second } - .filter { it.second > 5000 } - .map { it.first } - } - } - - private fun updateData(data: List) { - view?.run { - showEmpty(data.isEmpty()) - showContent(data.isNotEmpty()) - showErrorView(false) - updateData(data) - resetListPosition() - } - } - - private fun calculateMatchRatio(message: Message, query: String): Int { - val subjectRatio = FuzzySearch.tokenSortPartialRatio( - query.toLowerCase(Locale.getDefault()), - message.subject - ) - - val senderOrRecipientRatio = FuzzySearch.tokenSortPartialRatio( - query.toLowerCase(Locale.getDefault()), - if (message.sender.isNotEmpty()) message.sender.toLowerCase(Locale.getDefault()) - else message.recipient.toLowerCase(Locale.getDefault()) - ) - - val dateRatio = listOf( - FuzzySearch.ratio( - query.toLowerCase(Locale.getDefault()), - message.date.toFormattedString("dd.MM").toLowerCase(Locale.getDefault()) - ), - FuzzySearch.ratio( - query.toLowerCase(Locale.getDefault()), - message.date.toFormattedString("dd.MM.yyyy").toLowerCase(Locale.getDefault()) - ), - FuzzySearch.ratio( - query.toLowerCase(Locale.getDefault()), - message.date.toFormattedString("d MMMM").toLowerCase(Locale.getDefault()) - ), - FuzzySearch.ratio( - query.toLowerCase(Locale.getDefault()), - message.date.toFormattedString("d MMMM yyyy").toLowerCase(Locale.getDefault()) - ) - ).max() ?: 0 - - - return (subjectRatio.toDouble().pow(2) - + senderOrRecipientRatio.toDouble().pow(2) - + dateRatio.toDouble().pow(2) * 2 - ).toInt() - } } 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 f521191cf..92115ed33 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,19 +1,23 @@ 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 resetListPosition() + fun updateData(data: List) - fun updateData(data: List) + fun updateItem(item: AbstractFlexibleItem<*>) - fun updateItem(item: Message, position: Int) + fun clearView() 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 4bc3097d6..27c72ce69 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,38 +1,10 @@ package io.github.wulkanowy.ui.modules.mobiledevice -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible 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 @Inject constructor() : - RecyclerView.Adapter() { - - var items = mutableListOf() +class MobileDeviceAdapter> : FlexibleAdapter(null, null, true) { 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 6a3e5a441..d47574f60 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 androidx.core.view.postDelayed -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.Snackbar +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 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 io.github.wulkanowy.ui.widgets.DividerItemDecoration +import kotlinx.android.synthetic.main.fragment_mobile_device.* import javax.inject.Inject -class MobileDeviceFragment : - BaseFragment(R.layout.fragment_mobile_device), MobileDeviceView, - MainView.TitledView { +class MobileDeviceFragment : BaseFragment(), MobileDeviceView, MainView.TitledView { @Inject lateinit var presenter: MobileDevicePresenter @Inject - lateinit var devicesAdapter: MobileDeviceAdapter + lateinit var devicesAdapter: MobileDeviceAdapter> companion object { fun newInstance() = MobileDeviceFragment() @@ -35,95 +35,92 @@ class MobileDeviceFragment : get() = R.string.mobile_devices_title override val isViewEmpty: Boolean - get() = devicesAdapter.items.isEmpty() + get() = devicesAdapter.isEmpty - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentMobileDeviceBinding.bind(view) - messageContainer = binding.mobileDevicesRecycler + 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 presenter.onAttachView(this) } override fun initView() { - devicesAdapter.onDeviceUnregisterListener = presenter::onUnregisterDevice - - with(binding.mobileDevicesRecycler) { - layoutManager = LinearLayoutManager(context) + with(mobileDevicesRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = devicesAdapter - addItemDecoration(DividerItemDecoration(context)) + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider() + .withDrawDividerOnLastItem(false) + ) } - - 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) { - items = data.toMutableList() - notifyDataSetChanged() + isPermanentDelete = false + onDeviceUnregisterListener = presenter::onUnregisterDevice } + mobileDevicesSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + mobileDevicesErrorRetry.setOnClickListener { presenter.onRetry() } + mobileDevicesErrorDetails.setOnClickListener { presenter.onDetailsClick() } + mobileDeviceAddButton.setOnClickListener { presenter.onRegisterDevice() } } - override fun deleteItem(device: MobileDevice, position: Int) { - with(devicesAdapter) { - items.removeAt(position) - notifyItemRemoved(position) - notifyItemRangeChanged(position, itemCount) - } + override fun updateData(data: List) { + devicesAdapter.updateDataSet(data) } - override fun restoreDeleteItem(device: MobileDevice, position: Int) { - with(devicesAdapter) { - items.add(position, device) - notifyItemInserted(position) - notifyItemRangeChanged(position, itemCount) - } + override fun restoreDeleteItem() { + devicesAdapter.restoreDeletedItems() } - override fun showUndo(device: MobileDevice, position: Int) { - var confirmed = true + override fun clearData() { + devicesAdapter.clear() + } - Snackbar.make(binding.mobileDevicesRecycler, getString(R.string.mobile_device_removed), 3000) - .setAction(R.string.all_undo) { - confirmed = false - presenter.onUnregisterCancelled(device, position) - }.show() + override fun showUndo(position: Int, device: MobileDevice) { + val onActionListener = object : UndoHelper.OnActionListener { + override fun onActionConfirmed(action: Int, event: Int) { + presenter.onUnregisterConfirmed(device) + } - view?.postDelayed(3000) { - if (confirmed) presenter.onUnregisterConfirmed(device) + override fun onActionCanceled(action: Int, positions: MutableList?) { + presenter.onUnregisterCancelled() + } } + + 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 hideRefresh() { - binding.mobileDevicesSwipe.isRefreshing = false + mobileDevicesSwipe.isRefreshing = false } override fun showProgress(show: Boolean) { - binding.mobileDevicesProgress.visibility = if (show) VISIBLE else GONE + mobileDevicesProgress.visibility = if (show) VISIBLE else GONE } override fun showEmpty(show: Boolean) { - binding.mobileDevicesEmpty.visibility = if (show) VISIBLE else GONE + mobileDevicesEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.mobileDevicesError.visibility = if (show) VISIBLE else GONE + mobileDevicesError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.mobileDevicesErrorMessage.text = message + mobileDevicesErrorMessage.text = message } override fun enableSwipe(enable: Boolean) { - binding.mobileDevicesSwipe.isEnabled = enable + mobileDevicesSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.mobileDevicesRecycler.visibility = if (show) VISIBLE else GONE + 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 new file mode 100644 index 000000000..436c2d0e2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceItem.kt @@ -0,0 +1,53 @@ +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 new file mode 100644 index 000000000..59bbaa9f8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceModule.kt @@ -0,0 +1,12 @@ +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 459ca17e8..a6a83f8a9 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,6 +54,7 @@ class MobileDevicePresenter @Inject constructor( mobileDeviceRepository.getDevices(student, semester, forceRefresh) } } + .map { items -> items.map { MobileDeviceItem(it) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -70,12 +71,7 @@ class MobileDevicePresenter @Inject constructor( showEmpty(it.isEmpty()) showErrorView(false) } - analytics.logEvent( - "load_data", - "type" to "devices", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_devices", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading mobile devices result: An exception occurred") errorHandler.dispatch(it) @@ -99,15 +95,14 @@ class MobileDevicePresenter @Inject constructor( fun onUnregisterDevice(device: MobileDevice, position: Int) { view?.run { - deleteItem(device, position) - showUndo(device, position) + showUndo(position, device) showEmpty(isViewEmpty) } } - fun onUnregisterCancelled(device: MobileDevice, position: Int) { + fun onUnregisterCancelled() { view?.run { - restoreDeleteItem(device, position) + restoreDeleteItem() showEmpty(isViewEmpty) } } @@ -121,6 +116,7 @@ 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 ec2d3f87f..869b59bb1 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,16 +9,14 @@ interface MobileDeviceView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) - fun deleteItem(device: MobileDevice, position: Int) - - fun restoreDeleteItem(device: MobileDevice, position: Int) - - fun showUndo(device: MobileDevice, position: Int) + fun restoreDeleteItem() fun hideRefresh() + fun clearData() + fun showProgress(show: Boolean) fun enableSwipe(enable: Boolean) @@ -31,5 +29,7 @@ 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 9ac6049e9..8b81156b1 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(), } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_mobile_device, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -42,24 +42,24 @@ class MobileDeviceTokenDialog : BaseDialogFragment(), } override fun initView() { - binding.mobileDeviceDialogClose.setOnClickListener { dismiss() } + mobileDeviceDialogClose.setOnClickListener { dismiss() } } override fun updateData(token: MobileDeviceToken) { - with(binding.mobileDeviceDialogToken) { + with(mobileDeviceDialogToken) { text = token.token setOnClickListener { clickCopy(token.token) } } - with(binding.mobileDeviceDialogSymbol) { + with(mobileDeviceDialogSymbol) { text = token.symbol setOnClickListener { clickCopy(token.symbol) } } - with(binding.mobileDeviceDialogPin) { + with(mobileDeviceDialogPin) { text = token.pin setOnClickListener { clickCopy(token.pin) } } - binding.mobileDeviceQr.setImageBitmap(Base64.decode(token.qr, Base64.DEFAULT).let { + mobileDeviceQr.setImageBitmap(Base64.decode(token.qr, Base64.DEFAULT).let { BitmapFactory.decodeByteArray(it, 0, it.size) }) } @@ -71,11 +71,11 @@ class MobileDeviceTokenDialog : BaseDialogFragment(), } override fun hideLoading() { - binding.mobileDeviceDialogProgress.visibility = GONE + mobileDeviceDialogProgress.visibility = GONE } override fun showContent() { - binding.mobileDeviceDialogContent.visibility = VISIBLE + 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 deleted file mode 100644 index 70587b0cf..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt +++ /dev/null @@ -1,34 +0,0 @@ -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 a79087de2..ef9c36fab 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,10 +2,13 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import android.view.ViewGroup +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.FragmentMoreBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.about.AboutFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment @@ -18,16 +21,17 @@ 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(R.layout.fragment_more), MoreView, - MainView.TitledView, MainView.MainChildView { +class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: MorePresenter @Inject - lateinit var moreAdapter: MoreAdapter + lateinit var moreAdapter: FlexibleAdapter> companion object { fun newInstance() = MoreFragment() @@ -60,17 +64,20 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), override val aboutRes: Pair? get() = context?.run { getString(R.string.about_title) to getCompatDrawable(R.drawable.ic_all_about) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentMoreBinding.bind(view) + 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) presenter.onAttachView(this) } override fun initView() { - moreAdapter.onClickListener = presenter::onItemSelected + moreAdapter.setOnItemClickListener { presenter.onItemSelected(it) } - with(binding.moreRecycler) { - layoutManager = LinearLayoutManager(context) + moreRecycler.apply { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = moreAdapter } } @@ -79,11 +86,8 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), if (::presenter.isInitialized) presenter.onViewReselected() } - override fun updateData(data: List>) { - with(moreAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + moreAdapter.updateDataSet(data) } 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 new file mode 100644 index 000000000..85b604e77 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt @@ -0,0 +1,47 @@ +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 593645c19..096f89e9b 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,5 +1,6 @@ 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 @@ -20,10 +21,11 @@ class MorePresenter @Inject constructor( loadData() } - fun onItemSelected(title: String) { - Timber.i("Select more item \"${title}\"") + fun onItemSelected(item: AbstractFlexibleItem<*>?) { + if (item !is MoreItem) return + Timber.i("Select more item \"${item.title}\"") view?.run { - when (title) { + when (item.title) { messagesRes?.first -> openMessagesView() homeworkRes?.first -> openHomeworkView() noteRes?.first -> openNoteView() @@ -45,15 +47,15 @@ class MorePresenter @Inject constructor( Timber.i("Load items for more view") view?.run { updateData(listOfNotNull( - messagesRes, - homeworkRes, - noteRes, - luckyNumberRes, - mobileDevicesRes, - schoolAndTeachersRes, - settingsRes, - aboutRes - )) + 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) }) + ) } } } 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 922afdfd5..41008176d 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 deleted file mode 100644 index 2ffcad949..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteAdapter.kt +++ /dev/null @@ -1,60 +0,0 @@ -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 6d1b181ac..20ffea4eb 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,16 +9,13 @@ 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 { @@ -40,22 +37,19 @@ class NoteDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogNoteBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_note, container, false) } @SuppressLint("SetTextI18n") override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - with(binding) { - noteDialogDate.text = note.date.toFormattedString() - noteDialogCategory.text = note.category - noteDialogTeacher.text = note.teacher - noteDialogContent.text = note.content - } - + noteDialogDate.text = note.date.toFormattedString() + noteDialogCategory.text = note.category + noteDialogTeacher.text = note.teacher + noteDialogContent.text = note.content if (note.isPointsShow) { - with(binding.noteDialogPoints) { + with(noteDialogPoints) { text = "${if (note.points > 0) "+" else ""}${note.points}" setTextColor(when (CategoryType.getByValue(note.categoryType)) { CategoryType.POSITIVE -> ContextCompat.getColor(requireContext(), R.color.note_positive) @@ -64,7 +58,6 @@ class NoteDialog : DialogFragment() { }) } } - - binding.noteDialogClose.setOnClickListener { dismiss() } + 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 473381659..e5335e459 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,27 +1,31 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +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 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.ui.widgets.DividerItemDecoration +import io.github.wulkanowy.utils.setOnItemClickListener +import kotlinx.android.synthetic.main.fragment_note.* import javax.inject.Inject -class NoteFragment : BaseFragment(R.layout.fragment_note), NoteView, - MainView.TitledView { +class NoteFragment : BaseFragment(), NoteView, MainView.TitledView { @Inject lateinit var presenter: NotePresenter @Inject - lateinit var noteAdapter: NoteAdapter + lateinit var noteAdapter: FlexibleAdapter> companion object { fun newInstance() = NoteFragment() @@ -31,80 +35,77 @@ class NoteFragment : BaseFragment(R.layout.fragment_note), get() = R.string.note_title override val isViewEmpty: Boolean - get() = noteAdapter.items.isEmpty() + get() = noteAdapter.isEmpty - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentNoteBinding.bind(view) + 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) presenter.onAttachView(this) } override fun initView() { - noteAdapter.onClickListener = presenter::onNoteItemSelected + noteAdapter.run { + setOnItemClickListener { presenter.onNoteItemSelected(it) } + } - with(binding.noteRecycler) { - layoutManager = LinearLayoutManager(context) + noteRecycler.run { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = noteAdapter - addItemDecoration(DividerItemDecoration(context)) - } - with(binding) { - noteSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - noteErrorRetry.setOnClickListener { presenter.onRetry() } - noteErrorDetails.setOnClickListener { presenter.onDetailsClick() } + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider() + .withDrawDividerOnLastItem(false) + ) } + 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) { - with(noteAdapter) { - items = data.toMutableList() - notifyDataSetChanged() - } + override fun updateData(data: List) { + noteAdapter.updateDataSet(data, true) } - override fun updateItem(item: Note, position: Int) { - with(noteAdapter) { - items[position] = item - notifyItemChanged(position) - } + override fun updateItem(item: AbstractFlexibleItem<*>) { + noteAdapter.updateItem(item) } override fun clearData() { - with(noteAdapter) { - items = mutableListOf() - notifyDataSetChanged() - } + noteAdapter.clear() } override fun showEmpty(show: Boolean) { - binding.noteEmpty.visibility = if (show) VISIBLE else GONE + noteEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.noteError.visibility = if (show) VISIBLE else GONE + noteError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.noteErrorMessage.text = message + noteErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.noteProgress.visibility = if (show) VISIBLE else GONE + noteProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.noteSwipe.isEnabled = enable + noteSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.noteRecycler.visibility = if (show) VISIBLE else GONE + noteRecycler.visibility = if (show) VISIBLE else GONE } override fun hideRefresh() { - binding.noteSwipe.isRefreshing = false + 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 new file mode 100644 index 000000000..53fe6fa91 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteItem.kt @@ -0,0 +1,77 @@ +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 7d301c66b..7acf37a4c 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,5 +1,6 @@ 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 @@ -52,7 +53,8 @@ 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.sortedByDescending { it.date } } + .map { items -> items.map { NoteItem(it) } } + .map { items -> items.sortedByDescending { it.note.date } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -69,12 +71,7 @@ class NotePresenter @Inject constructor( showErrorView(false) showContent(it.isNotEmpty()) } - analytics.logEvent( - "load_data", - "type" to "note", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_note", "items" to it.size, "force_refresh" to forceRefresh) }, { Timber.i("Loading note result: An exception occurred") errorHandler.dispatch(it) @@ -93,14 +90,16 @@ class NotePresenter @Inject constructor( } } - 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) + 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) + } } } } 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 a7cbab8f4..a9c9f4f2f 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,5 +1,6 @@ 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 @@ -9,9 +10,9 @@ interface NoteView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) - fun updateItem(item: Note, position: Int) + fun updateItem(item: AbstractFlexibleItem<*>) 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 0245aa076..5f9c0b9a0 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,11 +1,12 @@ 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 @@ -13,11 +14,10 @@ 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(R.layout.fragment_schoolandteachers), - SchoolAndTeachersView, MainView.TitledView { +class SchoolAndTeachersFragment : BaseFragment(), SchoolAndTeachersView, MainView.TitledView { @Inject lateinit var presenter: SchoolAndTeachersPresenter @@ -31,44 +31,45 @@ class SchoolAndTeachersFragment : override val titleStringId: Int get() = R.string.schoolandteachers_title - override val currentPageIndex get() = binding.schoolandteachersViewPager.currentItem + override val currentPageIndex get() = schoolandteachersViewPager.currentItem - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentSchoolandteachersBinding.bind(view) + 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) presenter.onAttachView(this) } override fun initView() { with(pagerAdapter) { - containerId = binding.schoolandteachersViewPager.id + containerId = schoolandteachersViewPager.id addFragmentsWithTitle(mapOf( SchoolFragment.newInstance() to getString(R.string.school_title), TeacherFragment.newInstance() to getString(R.string.teachers_title) )) } - with(binding.schoolandteachersViewPager) { + with(schoolandteachersViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } - with(binding.schoolandteachersTabLayout) { - setupWithViewPager(binding.schoolandteachersViewPager) + with(schoolandteachersTabLayout) { + setupWithViewPager(schoolandteachersViewPager) setElevationCompat(context.dpToPx(4f)) } } override fun showContent(show: Boolean) { - with(binding) { - schoolandteachersViewPager.visibility = if (show) VISIBLE else INVISIBLE - schoolandteachersTabLayout.visibility = if (show) VISIBLE else INVISIBLE - } + schoolandteachersViewPager.visibility = if (show) VISIBLE else INVISIBLE + schoolandteachersTabLayout.visibility = if (show) VISIBLE else INVISIBLE } override fun showProgress(show: Boolean) { - binding.schoolandteachersProgress.visibility = if (show) VISIBLE else INVISIBLE + 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 722999f86..5a7c4cade 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(R.layout.fragment_school), SchoolView, - MainView.TitledView, SchoolAndTeachersChildView { +class SchoolFragment : BaseFragment(), SchoolView, MainView.TitledView, SchoolAndTeachersChildView { @Inject lateinit var presenter: SchoolPresenter override val titleStringId get() = R.string.school_title - override val isViewEmpty get() = binding.schoolName.text.isBlank() + override val isViewEmpty get() = schoolName.text.isBlank() companion object { fun newInstance() = SchoolFragment() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentSchoolBinding.bind(view) + 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) presenter.onAttachView(this) } override fun initView() { - with(binding) { - schoolSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - schoolErrorRetry.setOnClickListener { presenter.onRetry() } - schoolErrorDetails.setOnClickListener { presenter.onDetailsClick() } + 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) { - 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 - } + 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) { - binding.schoolEmpty.visibility = if (show) VISIBLE else GONE + schoolEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.schoolError.visibility = if (show) VISIBLE else GONE + schoolError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.schoolErrorMessage.text = message + schoolErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.schoolProgress.visibility = if (show) VISIBLE else GONE + schoolProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.schoolSwipe.isEnabled = enable + schoolSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.schoolContent.visibility = if (show) VISIBLE else GONE + schoolContent.visibility = if (show) VISIBLE else GONE } override fun hideRefresh() { - binding.schoolSwipe.isRefreshing = false + schoolSwipe.isRefreshing = false } override fun notifyParentDataLoaded() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolPresenter.kt index 7beff922d..e2eb614dc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolPresenter.kt @@ -88,11 +88,7 @@ class SchoolPresenter @Inject constructor( showEmpty(false) showErrorView(false) } - analytics.logEvent( - "load_item", - "type" to "school", - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_school", "force_refresh" to forceRefresh) }, { Timber.i("Loading school result: An exception occurred") errorHandler.dispatch(it) 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 deleted file mode 100644 index 8deeae05b..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherAdapter.kt +++ /dev/null @@ -1,40 +0,0 @@ -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 aa50339c1..b6bb9c101 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,28 +1,31 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +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 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 io.github.wulkanowy.ui.widgets.DividerItemDecoration +import kotlinx.android.synthetic.main.fragment_teacher.* import javax.inject.Inject -class TeacherFragment : BaseFragment(R.layout.fragment_teacher), - TeacherView, MainView.TitledView, SchoolAndTeachersChildView { +class TeacherFragment : BaseFragment(), TeacherView, MainView.TitledView, + SchoolAndTeachersChildView { @Inject lateinit var presenter: TeacherPresenter @Inject - lateinit var teacherAdapter: TeacherAdapter + lateinit var teacherAdapter: FlexibleAdapter> companion object { fun newInstance() = TeacherFragment() @@ -34,60 +37,69 @@ class TeacherFragment : BaseFragment(R.layout.fragment_t override val noSubjectString get() = getString(R.string.teacher_no_subject) override val isViewEmpty: Boolean - get() = teacherAdapter.items.isEmpty() + get() = teacherAdapter.isEmpty - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentTeacherBinding.bind(view) + 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) presenter.onAttachView(this) } override fun initView() { - with(binding.teacherRecycler) { - layoutManager = LinearLayoutManager(context) + teacherRecycler.run { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = teacherAdapter - addItemDecoration(DividerItemDecoration(context)) - } - with(binding) { - teacherSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } - teacherErrorRetry.setOnClickListener { presenter.onRetry() } - teacherErrorDetails.setOnClickListener { presenter.onDetailsClick() } + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider() + .withDrawDividerOnLastItem(false) + ) } + teacherSwipe.setOnRefreshListener { presenter.onSwipeRefresh() } + teacherErrorRetry.setOnClickListener { presenter.onRetry() } + teacherErrorDetails.setOnClickListener { presenter.onDetailsClick() } } - override fun updateData(data: List) { - with(teacherAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + teacherAdapter.updateDataSet(data, true) + } + + override fun updateItem(item: AbstractFlexibleItem<*>) { + teacherAdapter.updateItem(item) + } + + override fun clearData() { + teacherAdapter.clear() } override fun showEmpty(show: Boolean) { - binding.teacherEmpty.visibility = if (show) VISIBLE else GONE + teacherEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.teacherError.visibility = if (show) VISIBLE else GONE + teacherError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.teacherErrorMessage.text = message + teacherErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.teacherProgress.visibility = if (show) VISIBLE else GONE + teacherProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.teacherSwipe.isEnabled = enable + teacherSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.teacherRecycler.visibility = if (show) VISIBLE else GONE + teacherRecycler.visibility = if (show) VISIBLE else GONE } override fun hideRefresh() { - binding.teacherSwipe.isRefreshing = false + 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 new file mode 100644 index 000000000..368317744 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherItem.kt @@ -0,0 +1,59 @@ +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 0d8eec6d4..c5b6cfd69 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,6 +58,7 @@ 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 { @@ -75,12 +76,7 @@ class TeacherPresenter @Inject constructor( showEmpty(it.isEmpty()) showErrorView(false) } - analytics.logEvent( - "load_data", - "type" to "teachers", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_teachers", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading teachers result: An exception occurred") errorHandler.dispatch(it) 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 c655bfad8..b16be8ff9 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 io.github.wulkanowy.data.db.entities.Teacher +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersChildView @@ -12,10 +12,14 @@ interface TeacherView : BaseView, SchoolAndTeachersChildView { fun initView() - fun updateData(data: List) + fun updateData(data: List) + + fun updateItem(item: AbstractFlexibleItem<*>) 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 248417fd1..fe0a97632 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,8 +6,6 @@ import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import com.thelittlefireman.appkillermanager.AppKillerManager -import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException import com.yariksoffice.lingver.Lingver import dagger.android.support.AndroidSupportInjection import io.github.wulkanowy.R @@ -15,7 +13,6 @@ import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.openInternetBrowser import javax.inject.Inject class SettingsFragment : PreferenceFragmentCompat(), @@ -53,13 +50,6 @@ 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?) { @@ -101,19 +91,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) { @@ -129,23 +119,6 @@ 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) { _, _ -> - try { - AppKillerManager.doActionPowerSaving(requireContext()) - AppKillerManager.doActionAutoStart(requireContext()) - AppKillerManager.doActionNotification(requireContext()) - } catch (e: NoActionFoundException) { - requireContext().openInternetBrowser("https://dontkillmyapp.com/${AppKillerManager.getDevice()?.manufacturer}", ::showMessage) - } - } - .show() - } - override fun onResume() { super.onResume() preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) 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 bccb6f0bb..c8545ac0e 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,7 +4,6 @@ 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 @@ -21,7 +20,6 @@ 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, @@ -38,17 +36,17 @@ class SettingsPresenter @Inject constructor( fun onSharedPreferenceChanged(key: String) { Timber.i("Change settings $key") - preferencesRepository.apply { + with(preferencesRepository) { 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) @@ -58,10 +56,6 @@ 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 3786ba4b2..4a1b0c766 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,5 +19,4 @@ 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 23a437506..3e0106d1a 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,13 +3,12 @@ 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 deleted file mode 100644 index 85ded2025..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ /dev/null @@ -1,299 +0,0 @@ -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) { - val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(position)) - val until = lesson.until - val left = lesson.left - val isJustFinished = lesson.isJustFinished - - with(binding) { - when { - // before lesson - isShowTimeUntil -> { - Timber.d("Show time until lesson: $position") - timetableItemTimeLeft.visibility = GONE - with(timetableItemTimeUntil) { - visibility = VISIBLE - text = context.getString(R.string.timetable_time_until, - if (until.seconds <= 60) { - context.getString(R.string.timetable_seconds, until.seconds.toString(10)) - } else { - context.getString(R.string.timetable_minutes, until.toMinutes().toString(10)) - } - ) - } - } - // after lesson start - 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 (left.seconds < 60) { - context.getString(R.string.timetable_seconds, left.seconds.toString(10)) - } else { - context.getString(R.string.timetable_minutes, left.toMinutes().toString(10)) - } - ) - } - } - // right after lesson finish - 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 8efecf07c..57ec5998d 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,16 +11,13 @@ 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 { @@ -42,13 +39,13 @@ class TimetableDialog : DialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogTimetableBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_timetable, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - with(lesson) { + lesson.run { setInfo(info, teacher, canceled, changes) setSubject(subject, subjectOld) setTeacher(teacher, teacherOld) @@ -57,81 +54,74 @@ class TimetableDialog : DialogFragment() { setTime(start, end) } - binding.timetableDialogClose.setOnClickListener { dismiss() } + timetableDialogClose.setOnClickListener { dismiss() } } private fun setSubject(subject: String, subjectOld: String) { - 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 - } + 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) { - 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)) - } + 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() - } - } - else -> { - timetableDialogChangesTitle.visibility = GONE - timetableDialogChanges.visibility = GONE + 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 } } } private fun setTeacher(teacher: String, teacherOld: String) { - with(binding) { - when { - teacherOld.isNotBlank() && teacherOld != teacher -> { - timetableDialogTeacher.run { + when { + teacherOld.isNotBlank() && teacherOld != teacher -> { + timetableDialogTeacher.run { + visibility = VISIBLE + paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG + text = teacherOld + } + if (teacher.isNotBlank()) { + timetableDialogTeacherNew.run { visibility = VISIBLE - paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG - text = teacherOld - } - if (teacher.isNotBlank()) { - timetableDialogTeacherNew.run { - visibility = VISIBLE - text = teacher - } + 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) { - with(binding) { + group.let { when { - group.isNotBlank() -> timetableDialogGroup.text = group + it.isNotBlank() -> timetableDialogGroup.text = it else -> { timetableDialogGroupTitle.visibility = GONE timetableDialogGroup.visibility = GONE @@ -141,32 +131,30 @@ class TimetableDialog : DialogFragment() { } private fun setRoom(room: String, roomOld: String) { - with(binding) { - when { - roomOld.isNotBlank() && roomOld != room -> { - timetableDialogRoom.run { + when { + roomOld.isNotBlank() && roomOld != room -> { + timetableDialogRoom.run { + visibility = VISIBLE + paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG + text = roomOld + } + if (room.isNotBlank()) { + timetableDialogRoomNew.run { visibility = VISIBLE - paintFlags = paintFlags or STRIKE_THRU_TEXT_FLAG - text = roomOld - } - if (room.isNotBlank()) { - timetableDialogRoomNew.run { - visibility = VISIBLE - text = room - } + 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) { - binding.timetableDialogTime.text = "${start.toFormattedString("HH:mm")} - ${end.toFormattedString("HH:mm")}" + 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 2f01511ad..ef6057dfa 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,35 +1,40 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import android.view.ViewGroup 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(R.layout.fragment_timetable), - TimetableView, MainView.MainChildView, MainView.TitledView { +class TimetableFragment : BaseFragment(), TimetableView, MainView.MainChildView, + MainView.TitledView { @Inject lateinit var presenter: TimetablePresenter @Inject - lateinit var timetableAdapter: TimetableAdapter + lateinit var timetableAdapter: FlexibleAdapter> companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" @@ -39,7 +44,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme override val titleStringId get() = R.string.timetable_title - override val isViewEmpty get() = timetableAdapter.items.isEmpty() + override val isViewEmpty get() = timetableAdapter.isEmpty override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize @@ -48,33 +53,37 @@ class TimetableFragment : BaseFragment(R.layout.fragme setHasOptionsMenu(true) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentTimetableBinding.bind(view) - messageContainer = binding.timetableRecycler + 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 presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - timetableAdapter.onClickListener = presenter::onTimetableItemSelected + timetableAdapter.setOnItemClickListener(presenter::onTimetableItemSelected) - with(binding.timetableRecycler) { - layoutManager = LinearLayoutManager(context) + with(timetableRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = timetableAdapter - addItemDecoration(DividerItemDecoration(context)) + addItemDecoration(FlexibleItemDecoration(context) + .withDefaultDivider() + .withDrawDividerOnLastItem(false) + ) } - with(binding) { - timetableSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - timetableErrorRetry.setOnClickListener { presenter.onRetry() } - timetableErrorDetails.setOnClickListener { presenter.onDetailsClick() } + 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) { @@ -86,32 +95,24 @@ class TimetableFragment : BaseFragment(R.layout.fragme else false } - override fun updateData(data: List, showWholeClassPlanType: String, showTimetableTimers: Boolean) { - with(timetableAdapter) { - items = data.toMutableList() - showTimers = showTimetableTimers - showWholeClassPlan = showWholeClassPlanType - notifyDataSetChanged() - } + override fun updateData(data: List) { + timetableAdapter.updateDataSet(data, true) } override fun clearData() { - with(timetableAdapter) { - items = mutableListOf() - notifyDataSetChanged() - } + timetableAdapter.clear() } override fun updateNavigationDay(date: String) { - binding.timetableNavDate.text = date + timetableNavDate.text = date } override fun hideRefresh() { - binding.timetableSwipe.isRefreshing = false + timetableSwipe.isRefreshing = false } override fun resetView() { - binding.timetableRecycler.smoothScrollToPosition(0) + timetableRecycler.smoothScrollToPosition(0) } override fun onFragmentReselected() { @@ -123,35 +124,35 @@ class TimetableFragment : BaseFragment(R.layout.fragme } override fun showEmpty(show: Boolean) { - binding.timetableEmpty.visibility = if (show) VISIBLE else GONE + timetableEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.timetableError.visibility = if (show) VISIBLE else GONE + timetableError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.timetableErrorMessage.text = message + timetableErrorMessage.text = message } override fun showProgress(show: Boolean) { - binding.timetableProgress.visibility = if (show) VISIBLE else GONE + timetableProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.timetableSwipe.isEnabled = enable + timetableSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.timetableRecycler.visibility = if (show) VISIBLE else GONE + timetableRecycler.visibility = if (show) VISIBLE else GONE } override fun showPreButton(show: Boolean) { - binding.timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE + timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE } override fun showNextButton(show: Boolean) { - binding.timetableNextButton.visibility = if (show) VISIBLE else View.INVISIBLE + 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 new file mode 100644 index 000000000..5b35b85d1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt @@ -0,0 +1,197 @@ +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 e1ce005e3..2e9d0a0b3 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,6 +1,7 @@ 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 @@ -21,6 +22,7 @@ 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( @@ -100,9 +102,11 @@ class TimetablePresenter @Inject constructor( } } - fun onTimetableItemSelected(lesson: Timetable) { - Timber.i("Select timetable item ${lesson.id}") - view?.showTimetableDialog(lesson) + fun onTimetableItemSelected(item: AbstractFlexibleItem<*>?) { + if (item is TimetableItem) { + Timber.i("Select timetable item ${item.lesson.id}") + view?.showTimetableDialog(item.lesson) + } } fun onCompletedLessonsSwitchSelected(): Boolean { @@ -135,8 +139,9 @@ class TimetablePresenter @Inject constructor( timetableRepository.getTimetable(student, semester, currentDate, currentDate, forceRefresh) } } - .map { items -> items.filter { if (prefRepository.showWholeClassPlan == "no") it.isStudentPlan else true } } - .map { items -> items.sortedWith(compareBy({ it.number }, { !it.isStudentPlan })) } + .delay(200, MILLISECONDS) + .map { createTimetableItems(it) } + .map { items -> items.sortedWith(compareBy({ it.lesson.number }, { !it.lesson.isStudentPlan })) } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -149,17 +154,12 @@ class TimetablePresenter @Inject constructor( .subscribe({ Timber.i("Loading timetable result: Success") view?.apply { - updateData(it, prefRepository.showWholeClassPlan, prefRepository.showTimetableTimers) + updateData(it) showEmpty(it.isEmpty()) showErrorView(false) showContent(it.isNotEmpty()) } - analytics.logEvent( - "load_data", - "type" to "timetable", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_timetable", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading timetable result: An exception occurred") errorHandler.dispatch(it) @@ -178,6 +178,12 @@ 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 1efa320fc..f730a2712 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, showWholeClassPlanType: String, showTimetableTimers: Boolean) + fun updateData(data: List) 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 56ea16cfa..8f7b1ec5b 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,14 +6,12 @@ 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 io.github.wulkanowy.databinding.DialogLessonCompletedBinding -import io.github.wulkanowy.utils.lifecycleAwareVariable +import kotlinx.android.synthetic.main.dialog_lesson_completed.* class CompletedLessonDialog : DialogFragment() { - private var binding: DialogLessonCompletedBinding by lifecycleAwareVariable() - private lateinit var completedLesson: CompletedLesson companion object { @@ -30,54 +28,46 @@ class CompletedLessonDialog : DialogFragment() { super.onCreate(savedInstanceState) setStyle(STYLE_NO_TITLE, 0) arguments?.run { - completedLesson = getSerializable(ARGUMENT_KEY) as CompletedLesson + completedLesson = getSerializable(CompletedLessonDialog.ARGUMENT_KEY) as CompletedLesson } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogLessonCompletedBinding.inflate(inflater).apply { binding = this }.root + return inflater.inflate(R.layout.dialog_lesson_completed, container, false) } @SuppressLint("SetTextI18n") override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - 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 - } + 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()) { - with(binding) { - completedLessonDialogChangesTitle.visibility = View.GONE - completedLessonDialogChanges.visibility = View.GONE - } - } else binding.completedLessonDialogChanges.text = it + completedLessonDialogChangesTitle.visibility = View.GONE + completedLessonDialogChanges.visibility = View.GONE + } else completedLessonDialogChanges.text = it } completedLesson.absence.let { if (it.isBlank()) { - with(binding) { - completedLessonDialogAbsenceTitle.visibility = View.GONE - completedLessonDialogAbsence.visibility = View.GONE - } - } else binding.completedLessonDialogAbsence.text = it + completedLessonDialogAbsenceTitle.visibility = View.GONE + completedLessonDialogAbsence.visibility = View.GONE + } else completedLessonDialogAbsence.text = it } completedLesson.resources.let { if (it.isBlank()) { - with(binding) { - completedLessonDialogResourcesTitle.visibility = View.GONE - completedLessonDialogResources.visibility = View.GONE - } - } else binding.completedLessonDialogResources.text = it + completedLessonDialogResourcesTitle.visibility = View.GONE + completedLessonDialogResources.visibility = View.GONE + } else completedLessonDialogResources.text = it } - binding.completedLessonDialogClose.setOnClickListener { dismiss() } + 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 new file mode 100644 index 000000000..fd6dc8a66 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonItem.kt @@ -0,0 +1,58 @@ +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 deleted file mode 100644 index 3399a8a23..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsAdapter.kt +++ /dev/null @@ -1,45 +0,0 @@ -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 2efd30a34..60c16b2dc 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,34 +1,36 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import android.view.ViewGroup 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(R.layout.fragment_timetable_completed), - CompletedLessonsView, MainView.TitledView { +class CompletedLessonsFragment : BaseFragment(), CompletedLessonsView, MainView.TitledView { @Inject lateinit var presenter: CompletedLessonsPresenter @Inject - lateinit var completedLessonsAdapter: CompletedLessonsAdapter + lateinit var completedLessonsAdapter: FlexibleAdapter> companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" @@ -38,96 +40,88 @@ class CompletedLessonsFragment : override val titleStringId get() = R.string.completed_lessons_title - override val isViewEmpty get() = completedLessonsAdapter.items.isEmpty() + override val isViewEmpty get() = completedLessonsAdapter.isEmpty - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentTimetableCompletedBinding.bind(view) - messageContainer = binding.completedLessonsRecycler + 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 presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) } override fun initView() { - completedLessonsAdapter.onClickListener = presenter::onCompletedLessonsItemSelected + completedLessonsAdapter.setOnItemClickListener(presenter::onCompletedLessonsItemSelected) - with(binding.completedLessonsRecycler) { - layoutManager = LinearLayoutManager(context) + with(completedLessonsRecycler) { + layoutManager = SmoothScrollLinearLayoutManager(context) adapter = completedLessonsAdapter - addItemDecoration(DividerItemDecoration(context)) } - with(binding) { - completedLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) - completedLessonErrorRetry.setOnClickListener { presenter.onRetry() } - completedLessonErrorDetails.setOnClickListener { presenter.onDetailsClick() } + 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) { - with(completedLessonsAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + completedLessonsAdapter.updateDataSet(data, true) } override fun clearData() { - with(completedLessonsAdapter) { - items = emptyList() - notifyDataSetChanged() - } + completedLessonsAdapter.clear() } override fun updateNavigationDay(date: String) { - binding.completedLessonsNavDate.text = date + completedLessonsNavDate.text = date } override fun hideRefresh() { - binding.completedLessonsSwipe.isRefreshing = false + completedLessonsSwipe.isRefreshing = false } override fun showEmpty(show: Boolean) { - binding.completedLessonsEmpty.visibility = if (show) VISIBLE else GONE + completedLessonsEmpty.visibility = if (show) VISIBLE else GONE } override fun showErrorView(show: Boolean) { - binding.completedLessonError.visibility = if (show) VISIBLE else GONE + completedLessonError.visibility = if (show) VISIBLE else GONE } override fun setErrorDetails(message: String) { - binding.completedLessonErrorMessage.text = message + completedLessonErrorMessage.text = message } override fun showFeatureDisabled() { - with(binding) { - completedLessonsInfo.text = getString(R.string.error_feature_disabled) - completedLessonsInfoImage.setImageDrawable(requireContext().getCompatDrawable(R.drawable.ic_all_close_circle)) - } + completedLessonsInfo.text = getString(R.string.error_feature_disabled) + completedLessonsInfoImage.setImageDrawable(requireContext().getCompatDrawable(R.drawable.ic_all_close_circle)) } override fun showProgress(show: Boolean) { - binding.completedLessonsProgress.visibility = if (show) VISIBLE else GONE + completedLessonsProgress.visibility = if (show) VISIBLE else GONE } override fun enableSwipe(enable: Boolean) { - binding.completedLessonsSwipe.isEnabled = enable + completedLessonsSwipe.isEnabled = enable } override fun showContent(show: Boolean) { - binding.completedLessonsRecycler.visibility = if (show) VISIBLE else GONE + completedLessonsRecycler.visibility = if (show) VISIBLE else GONE } override fun showPreButton(show: Boolean) { - binding.completedLessonsPreviousButton.visibility = if (show) VISIBLE else INVISIBLE + completedLessonsPreviousButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showNextButton(show: Boolean) { - binding.completedLessonsNextButton.visibility = if (show) VISIBLE else INVISIBLE + 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 f72d753a9..001fed97b 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 io.github.wulkanowy.data.db.entities.CompletedLesson +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 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,6 +18,7 @@ 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( @@ -43,7 +44,6 @@ class CompletedLessonsPresenter @Inject constructor( completedLessonsErrorHandler.showErrorMessage = ::showErrorViewOnError completedLessonsErrorHandler.onFeatureDisabled = { this.view?.showFeatureDisabled() - this.view?.showEmpty(true) Timber.i("Completed lessons feature disabled by school") } loadData(ofEpochDay(date ?: baseDate.toEpochDay())) @@ -87,9 +87,11 @@ class CompletedLessonsPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - fun onCompletedLessonsItemSelected(completedLesson: CompletedLesson) { - Timber.i("Select completed lessons item ${completedLesson.id}") - view?.showCompletedLessonDialog(completedLesson) + fun onCompletedLessonsItemSelected(item: AbstractFlexibleItem<*>?) { + if (item is CompletedLessonItem) { + Timber.i("Select completed lessons item ${item.completedLesson.id}") + view?.showCompletedLessonDialog(item.completedLesson) + } } private fun setBaseDateOnHolidays() { @@ -117,7 +119,9 @@ class CompletedLessonsPresenter @Inject constructor( completedLessonsRepository.getCompletedLessons(student, semester, currentDate, currentDate, forceRefresh) } } - .map { items -> items.sortedBy { it.number } } + .delay(200, TimeUnit.MILLISECONDS) + .map { items -> items.map { CompletedLessonItem(it) } } + .map { items -> items.sortedBy { it.completedLesson.number } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .doFinally { @@ -135,12 +139,7 @@ class CompletedLessonsPresenter @Inject constructor( showErrorView(false) showContent(it.isNotEmpty()) } - analytics.logEvent( - "load_data", - "type" to "completed_lessons", - "items" to it.size, - "force_refresh" to forceRefresh - ) + analytics.logEvent("load_completed_lessons", "items" to it.size, "force_refresh" to forceRefresh) }) { Timber.i("Loading completed lessons result: An exception occurred") completedLessonsErrorHandler.dispatch(it) 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 170e19694..a6a327e2d 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 3dc7a3a8c..7636637f4 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,41 +4,36 @@ 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 androidx.recyclerview.widget.LinearLayoutManager +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.github.wulkanowy.R -import io.github.wulkanowy.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.AppInfo +import io.github.wulkanowy.utils.setOnItemClickListener +import kotlinx.android.synthetic.main.activity_widget_configure.* import javax.inject.Inject -class TimetableWidgetConfigureActivity : - BaseActivity(), +class TimetableWidgetConfigureActivity : BaseActivity(), TimetableWidgetConfigureView { @Inject - lateinit var configureAdapter: WidgetConfigureAdapter + lateinit var configureAdapter: FlexibleAdapter> @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(ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root) + setContentView(R.layout.activity_widget_configure) intent.extras.let { presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID), it?.getBoolean(EXTRA_FROM_PROVIDER)) @@ -46,20 +41,19 @@ class TimetableWidgetConfigureActivity : } override fun initView() { - with(binding.widgetConfigureRecycler) { + with(widgetConfigureRecycler) { adapter = configureAdapter - layoutManager = LinearLayoutManager(context) + layoutManager = SmoothScrollLinearLayoutManager(context) } - configureAdapter.onClickListener = presenter::onItemSelect + configureAdapter.setOnItemClickListener(presenter::onItemSelect) } override fun showThemeDialog() { - var items = arrayOf( + val items = arrayOf( getString(R.string.widget_timetable_theme_light), getString(R.string.widget_timetable_theme_dark) ) - if (appInfo.systemVersion >= 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) @@ -70,11 +64,8 @@ class TimetableWidgetConfigureActivity : .show() } - override fun updateData(data: List>) { - with(configureAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + configureAdapter.updateDataSet(data) } 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 new file mode 100644 index 000000000..a3338c684 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureItem.kt @@ -0,0 +1,59 @@ +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 57dde824c..e16851083 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,5 +1,6 @@ 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 @@ -31,11 +32,13 @@ class TimetableWidgetConfigurePresenter @Inject constructor( loadData() } - fun onItemSelect(student: Student) { - selectedStudent = student + fun onItemSelect(item: AbstractFlexibleItem<*>) { + if (item is TimetableWidgetConfigureItem) { + selectedStudent = item.student - if (isFromProvider) registerStudent(selectedStudent) - else view?.showThemeDialog() + if (isFromProvider) registerStudent(selectedStudent) + else view?.showThemeDialog() + } } fun onThemeSelect(index: Int) { @@ -45,7 +48,7 @@ class TimetableWidgetConfigurePresenter @Inject constructor( registerStudent(selectedStudent) } - fun onDismissThemeView() { + fun onDismissThemeView(){ view?.finishView() } @@ -53,7 +56,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 -> student to (student.id == currentStudentId) } + students.map { student -> TimetableWidgetConfigureItem(student, student.id == currentStudentId) } } .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) @@ -61,7 +64,7 @@ class TimetableWidgetConfigurePresenter @Inject constructor( when { it.isEmpty() -> view?.openLoginView() it.size == 1 && !isFromProvider -> { - selectedStudent = it.single().first + selectedStudent = it.single().student 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 056225ab5..7cac892d9 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,13 +1,12 @@ 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 dd223ad8b..fb8c59586 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,7 +41,9 @@ class TimetableWidgetFactory( private var lessons = emptyList() - private var savedCurrentTheme: Long? = null + private var savedTheme: Long? = null + + private var layoutId: Int? = null private var primaryColor: Int? = null @@ -69,32 +71,28 @@ class TimetableWidgetFactory( val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0) updateTheme(appWidgetId) + updateLessons(date, studentId) } } private fun updateTheme(appWidgetId: Int) { - savedCurrentTheme = sharedPref.getLong(getCurrentThemeWidgetKey(appWidgetId), 0) + savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) + layoutId = if (savedTheme == 0L) R.layout.item_widget_timetable else R.layout.item_widget_timetable_dark - 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 - } + 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 } private fun getItemLayout(lesson: Timetable): Int { return when { prefRepository.showWholeClassPlan == "small" && !lesson.isStudentPlan -> { - if (savedCurrentTheme == 0L) R.layout.item_widget_timetable_small + if (savedTheme == 0L) R.layout.item_widget_timetable_small else R.layout.item_widget_timetable_small_dark } - savedCurrentTheme == 1L -> R.layout.item_widget_timetable_dark - else -> R.layout.item_widget_timetable + savedTheme == 0L -> R.layout.item_widget_timetable + else -> R.layout.item_widget_timetable_dark } } 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 79888f83d..62192a1b0 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,7 +13,6 @@ 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 @@ -72,8 +71,6 @@ 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) { @@ -113,23 +110,14 @@ 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 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 savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) + val layoutId = if (savedTheme == 0L) R.layout.widget_timetable else R.layout.widget_timetable_dark val nextNavIntent = createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT) val prevNavIntent = createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV) @@ -162,11 +150,7 @@ class TimetableWidgetProvider : BroadcastReceiver() { setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) } - with(sharedPref) { - putLong(getCurrentThemeWidgetKey(appWidgetId), currentTheme) - putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) - } - + sharedPref.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 deleted file mode 100644 index b0b6999eb..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt +++ /dev/null @@ -1,27 +0,0 @@ -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 6912baa36..a444da6fe 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -4,6 +4,7 @@ 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 @@ -12,6 +13,8 @@ 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 cf715e657..582b1c835 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -9,8 +9,6 @@ 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.BuildConfig.APPLICATION_ID @ColorInt fun Context.getThemeAttrColor(@AttrRes colorAttr: Int): Int { @@ -22,11 +20,6 @@ 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) @@ -39,12 +32,6 @@ fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) - } } -fun Context.openAppInMarket(onActivityNotFound: (uri: String) -> Unit) { - openInternetBrowser("market://details?id=${APPLICATION_ID}") { - openInternetBrowser("https://github.com/wulkanowy/wulkanowy/releases", onActivityNotFound) - } -} - fun Context.openEmailClient(chooserTitle: String, email: String, subject: String, body: String, onActivityNotFound: () -> Unit = {}) { val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply { putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) @@ -71,17 +58,4 @@ fun Context.openDialer(phone: String) { startActivity(intent) } -fun Context.shareText(text: String, subject: String?) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - if (subject != null) { - putExtra(Intent.EXTRA_SUBJECT, subject) - } - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) -} - fun Context.dpToPx(dp: Float) = dp * resources.displayMetrics.densityDpi / DENSITY_DEFAULT diff --git a/app/src/main/java/io/github/wulkanowy/utils/FlexibleAdapterExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FlexibleAdapterExtension.kt new file mode 100644 index 000000000..bd6867a38 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/FlexibleAdapterExtension.kt @@ -0,0 +1,11 @@ +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/FragNavControlerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt index 9dc1e18a0..9cec331f8 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt @@ -4,14 +4,14 @@ import androidx.fragment.app.Fragment import com.ncapdevi.fragnav.FragNavController import io.github.wulkanowy.ui.modules.main.MainView -inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?, name: String?) -> Unit) { +inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?) -> Unit) { transactionListener = object : FragNavController.TransactionListener { override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + listener(fragment?.toSection()) } override fun onTabTransaction(fragment: Fragment?, index: Int) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + listener(fragment?.toSection()) } } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt deleted file mode 100644 index b96faeb21..000000000 --- a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt +++ /dev/null @@ -1,55 +0,0 @@ -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 lifecycleAwareVariable() = LifecycleAwareVariableActivity() diff --git a/app/src/main/java/io/github/wulkanowy/utils/SchooldaysRangeLimiter.kt b/app/src/main/java/io/github/wulkanowy/utils/SchooldaysRangeLimiter.kt index 46a707abb..922aafbd8 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SchooldaysRangeLimiter.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SchooldaysRangeLimiter.kt @@ -17,7 +17,7 @@ class SchooldaysRangeLimiter : DateRangeLimiter { override fun isOutOfRange(year: Int, month: Int, day: Int): Boolean { val date = LocalDate.of(year, month + 1, day) val dayOfWeek = date.dayOfWeek - return dayOfWeek == DayOfWeek.SUNDAY || date.isHolidays + return dayOfWeek == DayOfWeek.SUNDAY || dayOfWeek == DayOfWeek.SATURDAY || date.isHolidays } override fun getStartDate(): Calendar { diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index 63a30db8c..e4d4163b4 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -23,8 +23,6 @@ fun Sdk.init(student: Student): Sdk { certKey = student.certificateKey privateKey = student.privateKey - emptyCookieJarInterceptor = true - Timber.d("Sdk in ${student.loginMode} mode reinitialized") return this 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 802b2ee0f..a91f823fa 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt @@ -4,13 +4,9 @@ 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 @@ -22,10 +18,6 @@ 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)) @@ -92,8 +84,8 @@ inline val LocalDate.weekDayName: String inline val LocalDate.monday: LocalDate get() = with(MONDAY) -inline val LocalDate.sunday: LocalDate - get() = with(SUNDAY) +inline val LocalDate.friday: LocalDate + get() = with(FRIDAY) /** * [Dz.U. 2016 poz. 1335](http://prawo.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20160001335) diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt deleted file mode 100644 index ccb2afeb0..000000000 --- a/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt +++ /dev/null @@ -1,29 +0,0 @@ -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 68045da4a..6e72dd828 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,8 +1,6 @@ -Wersja 0.19.0 -- naprawiliśmy pokazywanie brakujących przedmiotów na liście podsumowania ocen -- ulepszyliśmy wygląd menadżera kont -- ulepszyliśmy wyszukiwarkę wiadomości -- dodaliśmy powiadomienia o proponowanych i końcowych ocenach -- dodaliśmy opcję udostępniania i drukowania wiadomości +- 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 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 deleted file mode 100644 index 201419d5d..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_stat_timetable.png and /dev/null 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 deleted file mode 100644 index dcfe95f26..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_stat_timetable.png and /dev/null 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 deleted file mode 100644 index 7264bd92a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_stat_timetable.png and /dev/null 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 deleted file mode 100644 index 1fb37b092..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png and /dev/null 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 deleted file mode 100644 index a95cc4f5d..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png and /dev/null 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 deleted file mode 100644 index 0f3326112..000000000 --- a/app/src/main/res/drawable/background_timetable_time_left.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_fullscreen.xml b/app/src/main/res/drawable/ic_fullscreen.xml deleted file mode 100644 index 86b7649b6..000000000 --- a/app/src/main/res/drawable/ic_fullscreen.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_fullscreen_exit.xml b/app/src/main/res/drawable/ic_fullscreen_exit.xml deleted file mode 100644 index bb7140f29..000000000 --- a/app/src/main/res/drawable/ic_fullscreen_exit.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_menu_message_print.xml b/app/src/main/res/drawable/ic_menu_message_print.xml deleted file mode 100644 index 204b0f6e3..000000000 --- a/app/src/main/res/drawable/ic_menu_message_print.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_menu_message_share.xml b/app/src/main/res/drawable/ic_menu_message_share.xml deleted file mode 100644 index 67a8ee494..000000000 --- a/app/src/main/res/drawable/ic_menu_message_share.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml deleted file mode 100644 index cd9985cb1..000000000 --- a/app/src/main/res/drawable/ic_search.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2ea0a4d39..d07dbbd8a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,5 @@ @@ -9,8 +8,7 @@ android:id="@+id/mainToolbar" style="@style/Widget.MaterialComponents.Toolbar.Surface" android:layout_width="match_parent" - android:layout_height="wrap_content" - app:contentInsetStartWithNavigation="0dp" /> + android:layout_height="wrap_content" /> - + + + android:layout_below="@id/accountDialogTitle" + android:overScrollMode="never" + tools:itemCount="3" + tools:listitem="@layout/item_account" /> - + - - - - - - - - - - - + diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 6747b7d0c..556bdfd09 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -1,12 +1,10 @@ + android:layout_height="match_parent" /> diff --git a/app/src/main/res/layout/fragment_attendance.xml b/app/src/main/res/layout/fragment_attendance.xml index 39b00d306..cd8b60364 100644 --- a/app/src/main/res/layout/fragment_attendance.xml +++ b/app/src/main/res/layout/fragment_attendance.xml @@ -153,8 +153,8 @@ android:background="?selectableItemBackgroundBorderless" android:fontFamily="sans-serif" android:gravity="center" - android:textSize="16sp" - tools:text="@tools:sample/date/ddmmyy" /> + android:text="@string/app_name" + android:textSize="16sp" /> + android:text="@string/app_name" + android:textSize="16sp" /> @@ -59,7 +59,9 @@ android:orientation="horizontal" android:paddingStart="16dp" android:paddingTop="5dp" - android:paddingEnd="16dp"> + android:paddingEnd="16dp" + android:visibility="invisible" + tools:visibility="visible"> + android:text="@string/app_name" + android:textSize="16sp" /> @@ -152,7 +151,6 @@ 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 d91d7b037..ba261e095 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -67,6 +67,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" + android:layout_marginRight="8dp" android:layout_weight="1" android:text="@string/login_contact_email" app:icon="@drawable/ic_more_messages" /> @@ -77,6 +78,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" + android:layout_marginLeft="8dp" android:layout_weight="1" android:text="@string/about_faq" app:icon="@drawable/ic_about_faq" /> @@ -132,7 +134,6 @@ 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" /> @@ -166,7 +167,6 @@ 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" @@ -180,6 +180,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="24dp" + android:layout_marginRight="24dp" android:text="@string/login_recover_button" android:textAppearance="?android:textAppearance" app:backgroundTint="?android:windowBackground" @@ -220,9 +221,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" + android:layout_marginRight="16dp" android:text="@string/login_advanced" android:textAppearance="?android:textAppearance" - android:visibility="gone" app:backgroundTint="?android:windowBackground" app:fontFamily="sans-serif-medium" app:layout_constraintBottom_toBottomOf="@id/loginFormSignIn" @@ -238,6 +239,7 @@ android:layout_gravity="center_vertical" android:layout_marginTop="48dp" android:layout_marginEnd="24dp" + android:layout_marginRight="24dp" android:layout_marginBottom="16dp" android:text="@string/login_sign_in" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/fragment_login_student_select.xml b/app/src/main/res/layout/fragment_login_student_select.xml index f9c6c1572..64e06cbd5 100644 --- a/app/src/main/res/layout/fragment_login_student_select.xml +++ b/app/src/main/res/layout/fragment_login_student_select.xml @@ -24,10 +24,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + android:visibility="gone" tools:visibility="visible"> - + android:layout_marginRight="16dp"> @@ -96,9 +95,9 @@ android:layout_height="wrap_content" android:layout_marginStart="32dp" android:layout_marginLeft="32dp" - android:layout_marginTop="32dp" android:layout_marginEnd="32dp" android:layout_marginRight="32dp" + android:layout_marginTop="32dp" android:layout_marginBottom="32dp" android:gravity="center_horizontal" android:text="@string/login_select_student" @@ -130,8 +129,8 @@ android:layout_height="wrap_content" android:layout_marginTop="32dp" android:layout_marginEnd="24dp" + android:layout_marginRight="24dp" android:layout_marginBottom="32dp" - android:enabled="false" android:text="@string/login_sign_in" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/fragment_timetable.xml b/app/src/main/res/layout/fragment_timetable.xml index 533af2fa1..98ccabbff 100644 --- a/app/src/main/res/layout/fragment_timetable.xml +++ b/app/src/main/res/layout/fragment_timetable.xml @@ -139,8 +139,8 @@ android:background="?selectableItemBackgroundBorderless" android:fontFamily="sans-serif" android:gravity="center" - android:textSize="16sp" - tools:text="@tools:sample/date/ddmmyy" /> + android:text="@string/app_name" + android:textSize="16sp" /> + app:srcCompat="@drawable/ic_chevron_left" + android:contentDescription="@string/all_prev"/> + android:text="@string/app_name" + android:textSize="16sp" /> + app:srcCompat="@drawable/ic_chevron_right" + android:contentDescription="@string/all_next" /> diff --git a/app/src/main/res/layout/header_account.xml b/app/src/main/res/layout/header_account.xml deleted file mode 100644 index 6219c26db..000000000 --- a/app/src/main/res/layout/header_account.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/header_exam.xml b/app/src/main/res/layout/header_exam.xml index 43f089f73..3a2e38c59 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.ExamAdapter"> + tools:context=".ui.modules.exam.ExamHeader"> diff --git a/app/src/main/res/layout/header_grade_details.xml b/app/src/main/res/layout/header_grade_details.xml index f2ba9a8c9..75e01745f 100644 --- a/app/src/main/res/layout/header_grade_details.xml +++ b/app/src/main/res/layout/header_grade_details.xml @@ -1,93 +1,81 @@ - + android:background="?selectableItemBackground" + android:paddingStart="16dp" + android:paddingLeft="16dp" + android:paddingTop="10dp" + android:paddingEnd="12dp" + android:paddingRight="14dp" + android:paddingBottom="10dp" + tools:context=".ui.modules.grade.details.GradeDetailsHeader"> - + android:layout_marginEnd="20dp" + android:layout_marginRight="20dp" + android:layout_toStartOf="@id/gradeHeaderNote" + android:layout_toLeftOf="@id/gradeHeaderNote" + android:ellipsize="end" + android:maxLines="1" + android:textSize="15sp" + tools:text="Fizyka" /> - + - + - + - - - - - - - + + diff --git a/app/src/main/res/layout/header_homework.xml b/app/src/main/res/layout/header_homework.xml index e1c6608f4..207fcdb40 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.HomeworkAdapter"> + tools:context=".ui.modules.homework.HomeworkHeader"> diff --git a/app/src/main/res/layout/item_about.xml b/app/src/main/res/layout/item_about.xml index f988c47ba..b5dc3758c 100644 --- a/app/src/main/res/layout/item_about.xml +++ b/app/src/main/res/layout/item_about.xml @@ -4,16 +4,17 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="72dp" - android:background="?selectableItemBackground" - tools:context=".ui.modules.about.AboutAdapter"> + android:background="?selectableItemBackground"> @@ -23,7 +24,9 @@ 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" @@ -36,11 +39,13 @@ 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 9568b345a..309be8121 100644 --- a/app/src/main/res/layout/item_account.xml +++ b/app/src/main/res/layout/item_account.xml @@ -1,67 +1,53 @@ - + android:paddingStart="24dp" + android:paddingLeft="24dp" + android:paddingEnd="24dp" + android:paddingRight="24dp" + tools:context=".ui.modules.account.AccountItem"> - - - + diff --git a/app/src/main/res/layout/item_attendance.xml b/app/src/main/res/layout/item_attendance.xml index 6917c6ce0..56bcab440 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.AttendanceAdapter"> + tools:context=".ui.modules.attendance.AttendanceItem"> + tools:visibility="visible" + tools:text="5" /> + android:orientation="vertical"> @@ -46,6 +46,7 @@ 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" /> @@ -56,7 +57,9 @@ 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" /> @@ -74,6 +77,7 @@ 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" /> @@ -84,7 +88,9 @@ 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" /> @@ -102,6 +108,7 @@ 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" /> @@ -112,7 +119,9 @@ 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" /> @@ -130,6 +139,7 @@ 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" /> @@ -140,7 +150,9 @@ 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" /> @@ -158,6 +170,7 @@ 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" /> @@ -168,7 +181,9 @@ 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" /> @@ -186,6 +201,7 @@ 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" /> @@ -196,7 +212,9 @@ 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" /> @@ -214,6 +232,7 @@ 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" /> @@ -224,7 +243,9 @@ 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 b9beec804..8b49ba765 100644 --- a/app/src/main/res/layout/item_completed_lesson.xml +++ b/app/src/main/res/layout/item_completed_lesson.xml @@ -3,12 +3,16 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?selectableItemBackground" + android:background="@drawable/ic_all_divider" + android:foreground="?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.CompletedLessonsAdapter"> + tools:context=".ui.modules.timetable.completed.CompletedLessonItem" + tools:ignore="UnusedAttribute"> + android:background="?selectableItemBackground" + android:orientation="vertical"> + android:layout_marginBottom="8dp"/> + tools:text="@tools:sample/lorem" + android:layout_centerVertical="true" + android:layout_toEndOf="@id/creatorItemAvatar" + android:layout_toRightOf="@id/creatorItemAvatar" + android:layout_marginLeft="16dp" + android:layout_marginStart="16dp" + android:layout_marginRight="16dp" + android:layout_marginEnd="16dp" /> diff --git a/app/src/main/res/layout/item_exam.xml b/app/src/main/res/layout/item_exam.xml index bd1d47312..459a7ecb3 100644 --- a/app/src/main/res/layout/item_exam.xml +++ b/app/src/main/res/layout/item_exam.xml @@ -4,13 +4,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - tools:context=".ui.modules.exam.ExamAdapter"> + tools:context=".ui.modules.exam.ExamItem"> @@ -21,6 +22,7 @@ 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" @@ -32,11 +34,15 @@ 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 ccc968c08..d6d99d9b8 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="?selectableItemBackground" + android:paddingStart="12dp" + android:paddingLeft="12dp" + android:paddingTop="7dp" + android:paddingEnd="12dp" + android:paddingRight="12dp" + android:paddingBottom="7dp" + tools:context=".ui.modules.grade.details.GradeDetailsItem"> - + + + android:layout_alignParentTop="true" + android:layout_marginStart="10dp" + android:layout_marginLeft="10dp" + android:layout_marginEnd="20dp" + android:layout_marginRight="20dp" + android:layout_toStartOf="@id/gradeItemNote" + android:layout_toLeftOf="@id/gradeItemNote" + android:layout_toEndOf="@+id/gradeItemValue" + android:layout_toRightOf="@+id/gradeItemValue" + android:ellipsize="end" + android:maxLines="1" + android:textSize="14sp" + tools:text="@tools:sample/lorem" /> - + - + - - - - - - - - + + diff --git a/app/src/main/res/layout/item_grade_summary.xml b/app/src/main/res/layout/item_grade_summary.xml index 85a78571e..a3a49ab62 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.GradeSummaryAdapter"> + tools:context=".ui.modules.grade.summary.GradeSummaryItem"> @@ -47,6 +48,7 @@ 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" /> @@ -57,7 +59,9 @@ 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" /> @@ -75,6 +79,7 @@ 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" /> @@ -85,7 +90,9 @@ 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" /> @@ -103,6 +110,7 @@ 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" /> @@ -113,7 +121,9 @@ 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 89c3c6885..62566e9c2 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.HomeworkAdapter"> + tools:context=".ui.modules.homework.HomeworkItem"> - - - - + android:paddingRight="16dp"> + android:paddingTop="8dp"> diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index 111de88c9..4379c2fc2 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -9,13 +9,14 @@ android:paddingTop="10dp" android:paddingRight="16dp" android:paddingBottom="10dp" - tools:context=".ui.modules.message.tab.MessageTabAdapter"> + tools:context=".ui.modules.message.MessageItem"> + tools:context=".ui.modules.mobiledevice.MobileDeviceItem"> + tools:context=".ui.modules.more.MoreItem"> diff --git a/app/src/main/res/layout/item_note.xml b/app/src/main/res/layout/item_note.xml index 660f32e00..3a56f9de7 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.NoteAdapter"> + tools:context=".ui.modules.note.NoteItem"> + tools:context=".ui.modules.schoolandteachers.teacher.TeacherItem"> + tools:context=".ui.modules.timetable.TimetableItem"> @@ -80,78 +86,35 @@ 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:ellipsize="end" + android:layout_marginRight="16dp" + android:layout_toEndOf="@id/timetableItemRoom" + android:layout_toRightOf="@id/timetableItemRoom" android:maxLines="1" + android:ellipsize="end" 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 98a213ec3..7a5ab033f 100644 --- a/app/src/main/res/layout/item_timetable_small.xml +++ b/app/src/main/res/layout/item_timetable_small.xml @@ -3,13 +3,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - tools:context=".ui.modules.timetable.TimetableAdapter"> + tools:context=".ui.modules.timetable.TimetableItem"> @@ -34,7 +37,9 @@ 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" @@ -46,8 +51,10 @@ 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" @@ -58,8 +65,10 @@ 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" @@ -71,9 +80,12 @@ 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 33f686dac..c69bade8a 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:paddingStart="6dp" - android:paddingLeft="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" + android:paddingLeft="6dp" + android:paddingStart="6dp" android:paddingEnd="12dp" - android:paddingRight="12dp" - android:paddingBottom="6dp"> + android:paddingRight="12dp"> + tools:visibility="gone"/> + android:paddingRight="12dp"> + tools:visibility="gone"/> - + android:orientation="vertical"> + android:orientation="vertical"> + android:orientation="horizontal"> - + \ 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 8a08b5d24..aa2269ad0 100644 --- a/app/src/main/res/layout/widget_timetable.xml +++ b/app/src/main/res/layout/widget_timetable.xml @@ -16,7 +16,9 @@ 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"> @@ -55,6 +60,7 @@ 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" @@ -66,6 +72,7 @@ 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 5533eaeee..3a301eb94 100644 --- a/app/src/main/res/layout/widget_timetable_dark.xml +++ b/app/src/main/res/layout/widget_timetable_dark.xml @@ -16,7 +16,9 @@ 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"> @@ -55,6 +60,7 @@ 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" @@ -66,6 +72,7 @@ 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_preview.xml b/app/src/main/res/menu/action_menu_message_preview.xml index 4c1332e10..dfc12e234 100644 --- a/app/src/main/res/menu/action_menu_message_preview.xml +++ b/app/src/main/res/menu/action_menu_message_preview.xml @@ -22,18 +22,4 @@ android:title="@string/message_delete" app:iconTint="@color/material_on_surface_emphasis_medium" app:showAsAction="ifRoom" /> - - diff --git a/app/src/main/res/menu/action_menu_message_tab.xml b/app/src/main/res/menu/action_menu_message_tab.xml deleted file mode 100644 index 18d348757..000000000 --- a/app/src/main/res/menu/action_menu_message_tab.xml +++ /dev/null @@ -1,11 +0,0 @@ - -

- - diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 11935b49b..8eae84194 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -1,18 +1,5 @@ - - Licht - Dunkel - Schwarz (AMOLED) - - - System Sprache - Polski - English - Pусский - Українська - Deutsch - 15 Minuten 30 Minuten @@ -22,6 +9,22 @@ 12 Stunden 24 Stunden + + + Licht + Dunkel + Schwarz (AMOLED) + + + + System Sprache + Polski + English + Pусский + Українська + Deutsch + + 0,0 0,25 @@ -29,16 +32,18 @@ 0,5 0,75 + Dzienniczek+ Wulkanowy Farben der Bewertungen im Logbuch + Durchschnittsnote für das 2. Semester - Durchschnitt der Noten aus beiden Semestern - Durchschnitt der Noten aus dem ganzen Jahr + 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 7cc44fd97..239c40437 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,5 +1,8 @@ + Wulkanowy + + Anmelden Wulkanowy @@ -18,8 +21,11 @@ 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 @@ -47,28 +53,27 @@ 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 @@ -103,26 +108,12 @@ Neue Note Neue Noten - - New predicted grade - New predicted grades - - - New final grade - New final grades - Du hast %1$d Note bekommen Du hast %1$d Noten bekommen - - You received %1$d predicted grade - You received %1$d predicted grades - - - You received %1$d final grade - You received %1$d final grades - + + Lektion Klassenzimmer @@ -130,21 +121,17 @@ 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 @@ -165,13 +152,19 @@ Abwesenheit erfolgreich entschuldigt! Sie müssen mindestens eine Abwesenheit auswählen! Verzeihung + + Schulbesuch Gesamt + + Diese Woche keine Prüfungen Form Eintrittsdatum + + Posteingang Gesendet @@ -188,8 +181,6 @@ In den Korb wandern Dauerhaft löschen Nachricht erfolgreich gelöscht - Share - Print Thema Inhalt Nachricht erfolgreich gesendet @@ -207,6 +198,8 @@ Du hast %1$d nachricht bekommen Du hast %1$d nachrichten bekommen + + Keine Informationen über Eintragen Punkte @@ -222,17 +215,23 @@ 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: %d + "Die heutige Glücksnummer ist: " + + Mobile Geräte Keine Geräte @@ -242,8 +241,12 @@ Token Symbol PIN + + Schule und Lehrer + + Schule Keine Informationen über die Schule @@ -254,19 +257,21 @@ 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 - Student account - Parent account - Mobile API mode - Hybrid mode + + Version der App Mitarbeiter @@ -283,17 +288,21 @@ Besuchen Sie die Website und helfen Sie bei der Entwicklung der Anwendung Lizenzen Lizenzen der in der Anwendung verwendeten Bibliotheken + + Lizenz - - Benutzerbild + + + + Avatar Sehen Sie mehr auf GitHub + Logs teilen Aktualisieren - - Auf Updates prüfen - Bevor Sie einen Fehler melden, prüfen Sie zuerst, ob ein Update mit der Fehlerbehebung verfügbar ist + + Inhalt Wiederhol @@ -310,14 +319,15 @@ Thema Zurück Nächste - Suchen - Suchen… + + Keine Lektionen Thema wählen Licht Dunkel - Systemthema + + Erscheinungsbild Standard Ansicht @@ -326,35 +336,23 @@ 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 @@ -362,8 +360,9 @@ Neue Nachrichten Neue Eintragen Push-Benachrichtigungen - Bevorstehende Lektionen Debuggen + + Schwarz Rot @@ -371,9 +370,13 @@ 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 a06a46f00..492645381 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -8,8 +8,6 @@ @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 1d81fb586..5553760a1 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -1,18 +1,5 @@ - - Jasny - Ciemny - Czarny (AMOLED) - - - Język systemu - Polski - English - Pусский - Українська - Deutsch - 15 minut 30 minut @@ -22,6 +9,22 @@ 12 godzin 24 godziny + + + Jasny + Ciemny + Czarny (AMOLED) + + + + Język systemu + Polski + English + Pусский + Українська + Deutsch + + 0,0 0,25 @@ -29,16 +32,18 @@ 0,5 0,75 + Dzienniczek+ Wulkanowy Kolory ocen w dzienniku + - Średnia ocen z drugiego semestru - Średnia średnich z obu semestrów - Średnia wszystkich ocen z całego roku + Ś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 bd4545e9b..742d61ed2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,5 +1,9 @@ + + Wulkanowy + + Logowanie Wulkanowy @@ -18,8 +22,11 @@ Uwagi i osiągnięcia Zadania domowe Wybierz konto + Semestr %d, %d/%d + + Zaloguj się za pomocą konta ucznia lub rodzica Podaj symbol @@ -28,13 +35,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 @@ -47,7 +54,7 @@ Nie znaleziono ucznia. Sprawdź symbol To pole jest wymagane Wybrany uczeń jest już zalogowany - Symbol znajdziesz na stronie dziennika w Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nUpewnij się, że w polu Dziennik UONET+ na poprzednim ekranie został ustawiony odpowiedni dziennik + Symbol znajdziesz na stronie dziennika w Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne Wybierz uczniów do zalogowania w aplikacji Inne opcje W tym trybie nie działa szczęśliwy numerek, uczeń na tle klasy, podsumowanie frekwencji, usprawiedliwianie nieobecności, lekcje zrealizowane, informacje o szkole i podgląd listy zarejestrowanych urządzeń @@ -58,17 +65,20 @@ 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 @@ -107,36 +117,14 @@ Nowe oceny Nowe oceny - - Nowa ocena przewidywana - Nowe oceny przewidywane - Nowe oceny przewidywane - Nowe oceny przewidywane - - - Nowa ocena końcowa - Nowe oceny końcowe - Nowe oceny końcowe - Nowe oceny końcowe - Masz %1$d nową ocenę Masz %1$d nowe oceny Masz %1$d nowych ocen Masz %1$d nowych ocen - - Masz %1$d nową przewidywaną ocenę - Masz %1$d nowe przewidywane oceny - Masz %1$d nowych przewidywanych ocen - Masz %1$d nowych przewidywanych ocen - - - Masz %1$d nową końcową ocenę - Masz %1$d nowe końcowe oceny - Masz %1$d nowych końcowych ocen - Masz %1$d nowych końcowych ocen - + + Lekcja Sala @@ -144,21 +132,17 @@ 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 @@ -181,13 +165,19 @@ Usprawiedliwiono pomyślnie! Musisz wybrać co najmniej jedną nieobecność! Usprawiedliw + + Frekwencja Razem + + Brak sprawdzianów w tym tygodniu Typ Data wpisu + + Odebrane Wysłane @@ -204,8 +194,6 @@ Przenieś do kosza Usuń trwale Wiadomość usunięta pomyślnie - Udostępnij - Drukuj Temat Treść Wiadomość wysłana pomyślnie @@ -229,6 +217,8 @@ Masz %1$d nowych wiadomości Masz %1$d nowych wiadomości + + Brak informacji o uwagach Punkty @@ -250,17 +240,23 @@ 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ń @@ -270,8 +266,12 @@ Token Symbol PIN + + Szkoła i nauczyciele + + Szkoła Brak informacji o szkole @@ -282,19 +282,21 @@ 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 - Konto ucznia - Konto rodzica - Tryb API mobilne - Tryb hybrydowy + + Wersja aplikacji Twórcy @@ -311,17 +313,21 @@ 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ż - - Sprawdź dostępność aktualizacji - Przed zgłoszeniem błędu sprawdź wcześniej, czy dostępna jest już aktualizacja z poprawką błędu + + Treść Ponów @@ -338,14 +344,15 @@ Przedmiot Poprzedni Następny - Szukaj - Szukaj… + + Brak lekcji Wybierz motyw Jasny Ciemny - Motyw systemu + + Wygląd Domyślny widok @@ -354,18 +361,14 @@ 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 @@ -376,22 +379,27 @@ 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 - Nowe oceny Szczęśliwy numerek + Nowe oceny Nowe wiadomości Nowe uwagi Powiadomienia push - Nadchodzące lekcje Debugowanie + + Czarny Czerwony @@ -399,9 +407,13 @@ 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 a41abf350..23cb5ac4d 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -1,5 +1,14 @@ + + 15 минут + 30 минут + 1 час + 2 часа + 6 часов + 12 часов + 24 часа + Светлая Тёмная @@ -13,15 +22,6 @@ Українська Deutsch - - 15 минут - 30 минут - 1 час - 2 часа - 6 часов - 12 часов - 24 часа - 0,0 0,25 @@ -36,9 +36,9 @@ Средняя оценка со 2 семестра - Average of grades from both semesters - Average of grades from the whole year + Средняя оценка с целого года + Не показывать Показать все diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 15c583173..e2f13f30c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,5 +1,9 @@ - + + + Wulkanowy + + Авторизация Wulkanowy @@ -8,18 +12,21 @@ Тесты Расписание Настройки - Другое + Ещё О приложении Просмотр журнала - Разработчики + Творцы Лицензии Сообщения Новое сообщение Предупреждения и свершения Домашние задания Выберите аккаунт + - %d семестр, %d/%d + Семестр %d, %d/%d + + Авторизируйтесь при помощи аккаунта ученика или родителя Впишите \"symbol\" @@ -28,73 +35,72 @@ Логин, PESEL или электронная почта Пароль Дневник UONET+ - Mobile API + Мобильный API Scraper - Hybrid + Гибрид Token PIN - Ключ API + клавиша API Symbol Войти Слишком короткий пароль - Указаны неверные данные. Убедитесь, что вы выбрали нужный дневник - Неправильный PIN - Неправильный token + Указаны неверные данные + Недействительный PIN + Недействительный token Токен просрочен Неверный адрес электронной почты Неправильный логин - Неправильный symbol - Не удалось найти ученика. Проверьте \"symbol\" - Обязательное поле + Недействительный symbol + Не удалось найти ученика. Пожалуйста, проверьте \"symbol\" + Это поле обязательно Данный ученик уже авторизован - Вы можете найти \"symbol\" на странице VULCAN по пути Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne + Вы можете найти \"symbol\" в Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne Выберите учеников для авторизации в приложении Другие варианты - В этом режиме не работают: счастливый номер, статистика класса по оценкам, статистика посещаемости и уроков, информация о школе и список зарегистрированных устройств - Scraper - режим, который показывает данные так же, как и сайт дневника - Hybrid - это комбинация лучших функций остальных двух режимов. Он работает быстрее, чем Scraper, и вводит функции, которых нет в режиме Mobile API. Находится в экспериментальной стадии Политика приватности Проблемы с авторизацией? Свяжитесь с нами! Электронная почта Discord Отправить письмо - Опишите детали проблемы: - Убедитесь, что выбран нужный дневник! - Забыли пароль? + восстановление + Я не помню пароль Восстановите свой аккаунт - Восстановить Студент уже вошел в систему + + Менеджер аккаунтов Войти Сеанс истёк Сеанс истёк, пожалуйста, авторизируйтесь ещё раз + + Оценка Семестр %d Сменить семестр Оценки отсутствуют - Стоимость - Стоимость: %s + Вес + Bес: %s Комментарий Новые оценки отсутствуют Количество новых оценок: %1$d Средняя оценка: %1$.2f - Баллы: %s + точек: %s Средняя оценка отсутствует Ожидаемая оценка: %1$s Итоговая оценка: %1$s - Сумма баллов + Сумма очков Итоговая оценка Ожидаемая оценка Рассчитанная средняя оценка Итоговая средняя оценка Итоги Класс - Пометить как \"прочитанное\" + Пометить как \"прочитанное* Частичные - За семестр - Баллы + Семестровые + Очки %d оценка %d оценки @@ -102,71 +108,45 @@ %d оценок - Новая оценка + Новая оценка Новые оценки Новые оценки Новые оценки - - New predicted grade - New predicted grades - New predicted grades - New predicted grades - - - New final grade - New final grades - New final grades - New final grades - Вы получили %1$d оценку Вы получили %1$d оценки Вы получили %1$d оценок Вы получили %1$d оценок - - You received %1$d predicted grade - You received %1$d predicted grades - You received %1$d predicted grades - You received %1$d predicted grades - - - You received %1$d final grade - You received %1$d final grades - You received %1$d final grades - You received %1$d final grades - + + Урок Аудитория Группа Часы Изменения - Нет уроков в данный день - %s мин - %s сек - %1$s осталось - через %1$s - Окончен - Сейчас: %s - Следующий: %s - Позже: %s - + Нету уроков в данный день + + + Проведённые уроки Просмотреть проведённые уроки Нет информации о проведённых уроках Тема Отсутствие Ресурсы + + Итоговая посещаемость Отсутствие по школьным причинам - Отсутствие по уважительной причине - Отсутствие по неуважительной причине + Оправданное отсутствие + Неоправданное отсутствие Освобождение - Опоздание по уважительным причинам - Опоздание по неуважительным причинам + Оправданное опоздание + Неоправданное опоздание Присутствие Урок № Данные не найдены @@ -178,23 +158,29 @@ Причина отсутствия (необязательно) Послать - Статус отсутствия изменён - Выберите хотя-бы одно отсутствие - Изменить статус + Отсутствие оправдано успешно! + Вы должны выбрать хотя бы одно отсутствие! + Oбосновывать + + Посещаемость - Общая + вместе + + - Нет тестов на этой неделе + Тесты на этой неделе не запланированы Тип Дата записи + + Полученные Отправленные Корзина - (нет темы) + (нету темы) Нет сообщений - Произошла ошибка во время получения содержания сообщения + Произошла ошибка во время получения текста сообщения От: Кому: Дата: %s @@ -204,8 +190,6 @@ Перенести в корзину Удалить навсегда Сообщение успешно удалено - Share - Print Тема Текст Сообщение успешно отправлено @@ -218,7 +202,7 @@ %d сообщений - Новое сообщение + Новое сообщение Новые сообщения Новые сообщения Новые сообщения @@ -229,9 +213,11 @@ Вы получили %1$d новых сообщений Вы получили %1$d новых сообщений + + Нет данных о предупреждениях - Баллы + точек %d предупреждение %d предупреждения @@ -239,7 +225,7 @@ %d предупреждений - Новое предупреждение + Новое предупреждение Новые предупреждения Новых предупреждений Новых предупреждений @@ -250,17 +236,23 @@ Вы получили %1$d предупреждений Вы получили %1$d предупреждений + + Нет домашних заданий сделанный Не сделано Вложения + + Счастливый номер Сегодняшний счастливый номер Нет данных о счастливом номере Сегодняшний счастливый номер Сегодняшний счастливый номер: %d + + Мобильные устройства Нет устройств @@ -270,8 +262,12 @@ Token Symbol PIN + + Школа и учителя + + Школа Нет информации о школе @@ -282,49 +278,56 @@ Педагог Показать на карте Позвонить + + Учителя Нет информации о учителях Нет предмета + + Добавить аккаунт Выйти Вы точно хотите выйти из данного аккаунта? Выйти - Student account - Parent account - Mobile API mode - Hybrid mode + + Версия приложения - Разработчики - Список разработчиков \"Wulkanowy\" - Возникла ошибка? - Сообщить о ошибке + Творцы + Список Wulkanowy программистов + Сообщить о ошибке + Отправить сообщение о ошибке через электронную почту FAQ - Часто задаваемые вопросы + Читайте часто задаваемые вопросы Сервер Discord Присоединиться к сообществу приложения Политика приватности - Правила хранения личных данных + Правила сбора личных данных Домашняя страница - Помочь в развитии приложения + Посетить страницу и помочь в развитии приложения Лицензии - Лицензии использованных библиотек + Лицензии использованных в приложении библиотек + + Лицензия - + + + Aватар - Страница проекта на GitHub + Смотрите больше на GitHub + + - Поделиться логами - Обновить - - Проверить наличие обновлений - Прежде чем сообщать об ошибке, проверьте наличие обновлений + Share logs + Обновление + + Содержание - Повторить + Снова Описание Нет описания Учитель @@ -338,60 +341,53 @@ Предмет Предыдущий Следующий - Поиск - Поиск… + + Нет уроков Выбрать тему Светлая Тёмная - Тема системы + + - Вид - Окно по умолчанию - Способ определения средней годовой оценки + Внешний вид + При входе открывать + Рассчитывание средней годовой оценки Принудительно высчитать среднюю оценку через приложение Показывать присутствия в посещаемости Тема приложения Больше оценок - Отмечать текущий урок в расписании - Показывать диаграммы в оценках класса Показать уроки всего класса Схема цветов оценок Язык приложения + Уведомления Показывать уведомления - Показывать уведомления о будущих уроках - Исправить проблемы с синхронизацией и уведомлениями - На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства. - Перейти в настройски Показывать дебаг-уведомления + Синхронизация Автоматическая синхронизация Приостановить синхронизации во время каникул Интервал синхронизации Только через Wi-Fi - Синхронизировать - Синхронизировано! - Синхронизация не удалась - Идёт синхронизация - Синхронизация - Ручная синхронизация не обновляет данные в приложении. - \nЧтобы увидеть обновлённые данные, перезапустите приложение. - + Другие - Стоимость плюса - Стоимость минуса - Отвечать с историей сообщений + Вес плюса + Вес минуса + Ответить с историей сообщений + + Новые данные в дневнике Новые оценки Счастливый номер Новые сообщения Новые заметки - Показывать push-уведомления - Будущие уроки + Push-уведомления Дебаг + + Чёрный Красный @@ -399,13 +395,17 @@ Зелёный Фиолетовый Нет цвета + + Скопировано Отменить + + Нет интернет-подключения Слишком долгое ожидание соединения с дневником - Авторизация не удалась. Попробуйте ещё раз или перезапустите дневник + Авторизация не удалась. Пожалуйста, попробуйте ещё раз или перезапустите дневник Требуется смена пароля Технический перерыв в журнале UONET + продолжается. Попробуйте позже Произошла неожиданная ошибка diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index 9942621a5..e09ae8cdd 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -1,5 +1,14 @@ + + 15 хвилин + 30 хвилин + 1 година + 2 години + 6 годин + 12 годин + 24 години + Світла Темна @@ -13,15 +22,6 @@ Українська Deutsch - - 15 хвилин - 30 хвилин - 1 година - 2 години - 6 годин - 12 годин - 24 години - 0,0 0,25 @@ -36,9 +36,9 @@ Середня оцінка з 2 семестру - Average of grades from both semesters - Average of grades from the whole year + Середня оцінка за весь рік + Не показувати Показати все diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 423c4e12f..d9db3f068 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,5 +1,9 @@ - + + + Wulkanowy + + Авторизація Wulkanowy @@ -8,80 +12,83 @@ Тести Розклад Налаштування - Інше + Ще Про додаток Переглядач журналів - Розробники + Творці Ліцензії Повідомлення Нове повідомлення Нотатки та досягнення Домашні завдання - Оберіть аккаунт + Виберіть акаунт + + - %d семестр, %d/%d + Семестр %d, %d/%d + + - Авторизуйтеся за допомогою аккаунта учня або батька + Авторизуйтеся за допомогою акаунта учня або батьків Впишіть \"symbol\" Ім\'я користувача Електронна пошта Логін, PESEL або електронна пошта Пароль Щоденник - Мobile API + Мобільний API Scraper - Hybrid + Гібрид Token PIN Ключ API Symbol - Увійти - Занадто короткий пароль - Вказані невірні дані. Впевніться, що ви увібрали потрібний щоденник - Неправильний PIN - Неправильний token - Минув термін дії токену + Ввійти + Дуже короткий пароль + Вказані неправильні дані + Недійсний PIN + Недійсний token Недійсна адреса електронної пошти - Неправильний логін - Неправильний symbol + Невірний логін + Токен протермінований + Недійсний symbol Не вдалося знайти учня. Будь ласка, перевірте \"symbol\" - Обов\'язкове поле - Даного учня вже авторизовано - Ви можете знайти \"symbol\" на сторінцi VULCAN стежкою Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne + Це поле обов\'язкове + Даний учень вже авторизований + Ви можете знайти \"symbol\" в 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 Сума балів @@ -89,9 +96,9 @@ Очікувана оцінка Розрахована середня оцінка Підсумкова середня оцінка - Підсумок + Підсумки Клас - Позначити як прочитане + Відмітити як \"прочитане* Поточні Семестрові Бали @@ -102,74 +109,48 @@ %d оцінок - Нова оцінка + Нова оцінка Нові оцінки Нові оцінки Нові оцінки - - New predicted grade - New predicted grades - New predicted grades - New predicted grades - - - New final grade - New final grades - New final grades - New final grades - Ви отримали %1$d оцінку Ви отримали %1$d оцінки Ви отримали %1$d оцінок - Ви отримали %1$d оцінок - - - You received %1$d predicted grade - You received %1$d predicted grades - You received %1$d predicted grades - You received %1$d predicted grades - - - You received %1$d final grade - You received %1$d final grades - You received %1$d final grades - You received %1$d final grades + ви отримали %1$d оцінок + + Урок Аудиторія Група Години Зміни - Брак уроків у цей день - %s хвилин - %s сек - %1$s лишилося - через %1$s - Завершено - Зараз: %s - Наступний: %s - Пізніше: %s - - Уроки, що відбулися - Показати уроки, що відбулися - Брак інформації о уроках, що відбулися + Цього дня уроків немає + + + + Проведені уроки + Подивитися проведені уроки + Інформації про проведені уроки немає Тема Відсутність Ресурси + + - Підсумок відвідуваності + Підсумкова відвідуваність Відсутність зі шкільних причин - Відсутність з поважних причин - Відсутність з не поважних причин + Виправдана відсутність + Невиправдана відсутність Звільнення - Спізнення з поважних причин - Спізнення з не поважних причин + Виправдане запізнення + Невиправдане запізнення Присутність - Номер уроку - Брак записів + урок № + Дані не знайдені %1$d відсутність %1$d відсутності @@ -178,39 +159,43 @@ Причина відсутності (необов’язково) Надіслати - Змінено статус відсутності - Оберіть хоча б одну відсутність - Змінити статус + Відсутність виправдана успішно! + Ви повинні вибрати хоча б одну відсутність! + Oбґрунтовувати + + Відвідуваність Загальна + + - Брак тестів у цьому тижні + Тести на цьому тижні не заплановані Тип Дата запису + + Отримані Відправлені Кошик - (брак теми) + (нема теми) Нема повідомлень - З\'явилася помилка підчас завантаження змісту повідомлення + Сталася помилка під час отримання тексту повідомлення Від: Кому: Дата: %s - Відповісти + Відповідь Переслати Видалити - Перемістити у кошик + Перемістити в кошик Видалити назавжди - Повідомлення було успішно видалено - Share - Print + Повідомлення успішно видалено Тема Зміст - Повідомлення було успішно відправлено - Необхідно обрати принаймні 1 адресата - Зміст повідомлення мусить складатися принаймні з 3 знаків + Повідомлення успішно відправлено + Ви повинні вибрати щонайменше одного отримувача + Текст повідомлення повинен містити щонайменше 3 знаки %d повідомлення %d повідомлення @@ -218,7 +203,7 @@ %d повідомлень - Нове повідомлення + Нове повідомлення Нові повідомлення Нові повідомлення Нові повідомлення @@ -229,104 +214,123 @@ Ви отримали %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 + + Школа та вчителі + + Школа - Брак інформації про школу + Немає інформації про школу Назва школи Адреса школи Телефон Директор Викладач - Показати на мапі + Показати на карті Зателефонувати + + Вчителі - Брак інформації про вчителів - Брак предмету + Немає інформації про вчителів + Нема предмету + + - Додати аккаунт + Додати акаунт Вийти - Ви впевнені, що хочете вийти з цього аккаунту? - Вийти з аккаунту учня - Student account - Parent account - Mobile API mode - Hybrid mode + Ви дійсно бажаєте вийти з даного акаунту? + Вийти + + - Версія додатка - Розробники - Список розробників \"Wulkanowy\" - Виникла помилка? - Повідомити о помилці за допомогою e-mail + Версія додатку + Tворці + Список Wulkanowy програмістів + Повідомити про помилку + Відправити повідомлення про помилку електронною поштою FAQ - Запитання, які часто задають + Читайте запитання, які часто задають Сервер Discord Приєднатися до спільноти додатка - Політика конфіденційності - Правила зберігання особистих даних + Політика приватності + Правила збору особистих даних Домашня сторінка - Допомогти розвитку додатка + Відвідати сторінку та допомогти в розвитку додатку Ліцензії - Ліцензії вжитих бібліотек + Ліцензії використаних в додатку бібліотек + + Ліцензія - + + + Аватар - Сторінка проекту на GitHub + Дивіться більше на GitHub + + - Поділитися логами + Поділитися журналами Оновити - - Провірити наявність оновлень - Перед тим, як повідомлювати о помілці, перевірте наявність оновлень + + Зміст - Повторити + Знову Опис - Брак опису + Нема опису Вчитель Дата Дата запису @@ -338,74 +342,71 @@ Предмет Попередній Наступний - Пошук - Пошук… + + - Брак уроків - Увібрати тему - Яскрава + Немає уроків + Вибрати тему + Світла Темна - Тема системи + + - Вигляд - Вікно за замовчуванням - Спосіб облічування оцінки на кінець року + Зовнішній вигляд + При вході відкривати + Розрахунок середньої річної оцінки Примусово розрахувати середню оцінку через додаток Показувати присутність у відвідуваності Тема додатку Більше оцінок - Позначити поточний урок у розкладі - Показувати діаграми в оцінках класу Показати уроки всього класу - Схема кольорів оцінок + Колірна гама оцінок Мова додатку + Повідомлення Показувати повідомлення - Показувати повідомлення о наступних уроках - Виправити помилки з синхронізацією і повідомленнями - На вашому пристрої можуть бути помилки з синхронізацією і повідомленнями\n\nЩоб виправити іх, вам необхідно додати Wulkanowy в авто-старт и вимкнути оптимізацію/экономію батареї в налаштуваннях пристрою. - Перейти до налаштувань Показувати дебаг-повідомлення + Синхронізація Автоматична синхронізація - Призупинено на час канікул - Інтервал оновлення + Призупинити синхронізації на час канікул + Інтервал синхронізації Тільки через Wi-Fi - Синхронізувати - Синхронізовано! - Синхронізація не вдалася - Триває синхронізація - Синхронізація - Ручна синхронізація не оновлює дані в додатку. - \nЩоб побачити оновлені дані, перезавантажте додаток. - + Інші - Вартість плюсу + Вага плюса Вага мінуса Відповісти з історією повідомлень + + Нові дані в щоденнику - Нові оцінки Щасливий номер + Нові оцінки Нові повідомлення Нові нотатки - Показувати push-повідомлення - Наступні уроки + Повідомлення pushs Дебаг + + Чорний Червоний Голубий Зелений Фіолетовий - Брак кольору + Нема кольору + + Скопійовано Відмінити + + - Брак з\'єднання з інтернетом + Відсутнє інтернет-підключення Занадто довге очікування з\'єднання з щоденником - Аутентифікація не вдалася. Спробуйте ще раз або запустіть додаток знову + Авторизація не відбулася. Будь ласка, авторизуйтеся знову або перезавантажте щоденник Потрібно змінити пароль Технічна перерва в журналі UONET + продовжується. Спробуйте пізніше Відбулася несподівана помилка diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index f661b88c5..5ce26b1d6 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.tk/?standard + http://fakelog.cf/?standard Default diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index a82b14eb7..be31b440f 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -5,7 +5,6 @@ only_one_semester false false - false light vulcan system @@ -13,11 +12,9 @@ 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 6cb877ec2..559e2159c 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -7,19 +7,15 @@ 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/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 5824658c4..76427a487 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -88,12 +88,10 @@ Average of grades only from the 2nd semester - Average of grades from both semesters Average of grades from the whole year only_one_semester - both_semesters all_year diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1eaebf284..c7b41f698 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,9 @@ + + Wulkanowy + + Login Wulkanowy @@ -51,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 @@ -113,26 +117,10 @@ New grade New grades - - New predicted grade - New predicted grades - - - New final grade - New final grades - You received %1$d grade You received %1$d grades - - You received %1$d predicted grade - You received %1$d predicted grades - - - You received %1$d final grade - You received %1$d final grades - @@ -142,14 +130,6 @@ Hours Changes No lessons this day - %s min - %s sec - %1$s left - in %1$s - Finished - Now: %s - Next: %s - Later: %s @@ -210,8 +190,6 @@ Move to trash Delete permanently Message deleted successfully - Share - Print Subject Content Message sent successfully @@ -300,10 +278,6 @@ Logout Do you want to log out of an active student? Student logout - Student account - Parent account - Mobile API mode - Hybrid mode @@ -339,11 +313,6 @@ Refresh - - Check for updates - Before reporting a bug, check first if an update with the bug fix is available - - Content Retry @@ -360,8 +329,6 @@ Subject Prev Next - Search - Search… @@ -369,7 +336,6 @@ Choose theme Light Dark - System Theme @@ -380,18 +346,12 @@ 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 @@ -422,7 +382,6 @@ 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 4cdb989c7..bb9eb5fcd 100644 --- a/app/src/main/res/xml/scheme_preferences.xml +++ b/app/src/main/res/xml/scheme_preferences.xml @@ -1,4 +1,4 @@ - + - - + app:title="@string/pref_services_force_sync" + app:key="@string/pref_key_services_force_sync" /> - - diff --git a/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt b/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt index e87177c1d..ef269ec73 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt @@ -1,48 +1,36 @@ package io.github.wulkanowy.utils +import android.content.Context import android.util.Log -import com.google.firebase.crashlytics.FirebaseCrashlytics -import fr.bipi.tressence.base.FormatterPriorityTree -import fr.bipi.tressence.common.StackTraceRecorder +import com.crashlytics.android.Crashlytics +import com.crashlytics.android.core.CrashlyticsCore +import fr.bipi.tressence.crash.CrashlyticsLogExceptionTree +import fr.bipi.tressence.crash.CrashlyticsLogTree +import io.fabric.sdk.android.Fabric import io.github.wulkanowy.sdk.exception.FeatureDisabledException import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException -import java.io.InterruptedIOException -import java.net.SocketTimeoutException import java.net.UnknownHostException -class CrashlyticsTree : FormatterPriorityTree(Log.VERBOSE) { +fun initCrashlytics(context: Context, appInfo: AppInfo) { + Fabric.with(Fabric.Builder(context) + .kits( + Crashlytics.Builder() + .core(CrashlyticsCore.Builder() + .disabled(!appInfo.isCrashlyticsEnabled) + .build()) + .build() + ) + .debuggable(appInfo.isDebug) + .build()) +} - private val crashlytics by lazy { FirebaseCrashlytics.getInstance() } +class CrashlyticsTree : CrashlyticsLogTree(Log.VERBOSE) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - if (skipLog(priority, tag, message, t)) return + if (t is FeatureDisabledException || t is FeatureNotAvailableException || t is UnknownHostException) return - crashlytics.log(format(priority, tag, message)) + super.log(priority, tag, message, t) } } -class CrashlyticsExceptionTree : FormatterPriorityTree(Log.ERROR) { - - private val crashlytics by lazy { FirebaseCrashlytics.getInstance() } - - override fun skipLog(priority: Int, tag: String?, message: String, t: Throwable?): Boolean { - if (t is FeatureDisabledException || t is FeatureNotAvailableException || t is UnknownHostException || t is SocketTimeoutException || t is InterruptedIOException) { - return true - } - - return super.skipLog(priority, tag, message, t) - } - - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - if (skipLog(priority, tag, message, t)) return - - crashlytics.setCustomKey("priority", priority) - crashlytics.setCustomKey("tag", tag.orEmpty()) - crashlytics.setCustomKey("message", message) - if (t != null) { - crashlytics.recordException(t) - } else { - crashlytics.recordException(StackTraceRecorder(format(priority, tag, message))) - } - } -} +class CrashlyticsExceptionTree : CrashlyticsLogExceptionTree() diff --git a/app/src/play/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt index b0b2fda4d..6810e66ac 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.utils -import android.app.Activity import android.content.Context import android.os.Bundle import com.google.firebase.analytics.FirebaseAnalytics @@ -25,8 +24,4 @@ class FirebaseAnalyticsHelper @Inject constructor(private val context: Context) analytics.logEvent(name, this) } } - - fun setCurrentScreen(activity: Activity, name: String?) { - analytics.setCurrentScreen(activity, name, null) - } } diff --git a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TestEnityCreator.kt similarity index 54% rename from app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt rename to app/src/test/java/io/github/wulkanowy/data/repositories/TestEnityCreator.kt index f7d29220f..175242bb8 100644 --- a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TestEnityCreator.kt @@ -1,12 +1,9 @@ -package io.github.wulkanowy +package io.github.wulkanowy.data.repositories -import io.github.wulkanowy.data.db.entities.Message 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.sdk.Sdk import org.threeten.bp.LocalDate -import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime.now fun createSemesterEntity(diaryId: Int, semesterId: Int, start: LocalDate, end: LocalDate, semesterName: Int = 1): Semester { @@ -48,50 +45,3 @@ fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.API): Student { userLoginId = 0 ) } - -fun getTimetableEntity( - isStudentPlan: Boolean = false, - canceled: Boolean = false, - start: LocalDateTime = now(), - end: LocalDateTime = now() -) = Timetable( - studentId = 0, - subject = "", - number = 0, - diaryId = 0, - canceled = canceled, - changes = false, - date = LocalDate.now(), - end = end, - group = "", - info = "", - isStudentPlan = isStudentPlan, - room = "", - roomOld = "", - start = start, - subjectOld = "", - teacher = "", - teacherOld = "" -) - -fun getMessageEntity( - messageId: Int, - content: String, - unread: Boolean -) = Message( - studentId = 1, - realId = 1, - messageId = messageId, - sender = "", - senderId = 1, - recipient = "", - subject = "", - content = content, - date = now(), - folderId = 1, - unread = unread, - unreadBy = 1, - readBy = 1, - removed = false, - hasAttachments = false -) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/attendance/AttendanceRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/attendance/AttendanceRemoteTest.kt index a5b79cfdd..b684ffc5f 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/attendance/AttendanceRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/attendance/AttendanceRemoteTest.kt @@ -1,7 +1,8 @@ package io.github.wulkanowy.data.repositories.attendance import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.getStudentEntity +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.repositories.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Attendance import io.github.wulkanowy.utils.init diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/completedlessons/CompletedLessonsRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/completedlessons/CompletedLessonsRemoteTest.kt index 10307feee..6a93067f2 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/completedlessons/CompletedLessonsRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/completedlessons/CompletedLessonsRemoteTest.kt @@ -1,7 +1,7 @@ package io.github.wulkanowy.data.repositories.completedlessons import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.getStudentEntity +import io.github.wulkanowy.data.repositories.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.CompletedLesson import io.github.wulkanowy.utils.init diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/exam/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/exam/ExamRemoteTest.kt index aecef4728..d2b06af78 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/exam/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/exam/ExamRemoteTest.kt @@ -1,7 +1,7 @@ package io.github.wulkanowy.data.repositories.exam import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.getStudentEntity +import io.github.wulkanowy.data.repositories.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Exam import io.github.wulkanowy.utils.init diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRemoteTest.kt index 77c87025d..7e481ee47 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/gradestatistics/GradeStatisticsRemoteTest.kt @@ -1,7 +1,7 @@ package io.github.wulkanowy.data.repositories.gradestatistics import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.getStudentEntity +import io.github.wulkanowy.data.repositories.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.GradePointsStatistics import io.github.wulkanowy.sdk.pojo.GradeStatistics diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/luckynumber/LuckyNumberRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/luckynumber/LuckyNumberRemoteTest.kt index cbdf34fa8..904e8c18c 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/luckynumber/LuckyNumberRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/luckynumber/LuckyNumberRemoteTest.kt @@ -1,6 +1,6 @@ package io.github.wulkanowy.data.repositories.luckynumber -import io.github.wulkanowy.getStudentEntity +import io.github.wulkanowy.data.repositories.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.mockk.MockKAnnotations diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/message/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/message/MessageRepositoryTest.kt index fcc4188a6..b7963d430 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/message/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/message/MessageRepositoryTest.kt @@ -2,10 +2,10 @@ package io.github.wulkanowy.data.repositories.message import androidx.room.EmptyResultSetException import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings +import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.UnitTestInternetObservingStrategy -import io.github.wulkanowy.getMessageEntity import io.reactivex.Single import io.reactivex.observers.TestObserver import org.junit.Assert.assertEquals @@ -15,6 +15,7 @@ import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.threeten.bp.LocalDateTime.now import java.net.UnknownHostException class MessageRepositoryTest { @@ -43,7 +44,7 @@ class MessageRepositoryTest { @Test fun `throw error when message is not in the db`() { - val testMessage = getMessageEntity(1, "", false) + val testMessage = Message(1, 1, 1, "", 1, "", "", "", now(), 1, false, 1, 1, false, false) `when`(local.getMessageWithAttachment(student, testMessage)).thenReturn(Single.error(EmptyResultSetException("No message in database"))) val message = repo.getMessage(student, testMessage) @@ -54,7 +55,7 @@ class MessageRepositoryTest { @Test fun `get message when content already in db`() { - val testMessage = getMessageEntity(123, "Test", false) + val testMessage = Message(1, 1, 123, "", 1, "", "", "Test", now(), 1, false, 1, 1, false, false) val messageWithAttachment = MessageWithAttachment(testMessage, emptyList()) `when`(local.getMessageWithAttachment(student, testMessage)).thenReturn(Single.just(messageWithAttachment)) @@ -66,7 +67,7 @@ class MessageRepositoryTest { @Test fun `get message when content in db is empty`() { - val testMessage = getMessageEntity(123, "", true) + val testMessage = Message(1, 1, 123, "", 1, "", "", "", now(), 1, true, 1, 1, false, false) val testMessageWithContent = testMessage.copy(content = "Test") val mWa = MessageWithAttachment(testMessage, emptyList()) @@ -85,7 +86,7 @@ class MessageRepositoryTest { @Test fun `get message when content in db is empty and there is no internet connection`() { - val testMessage = getMessageEntity(123, "", false) + val testMessage = Message(1, 1, 123, "", 1, "", "", "", now(), 1, false, 1, 1, false, false) val messageWithAttachment = MessageWithAttachment(testMessage, emptyList()) testObservingStrategy.isInternetConnection = false @@ -99,7 +100,7 @@ class MessageRepositoryTest { @Test fun `get message when content in db is empty, unread and there is no internet connection`() { - val testMessage = getMessageEntity(123, "", true) + val testMessage = Message(1, 1, 123, "", 1, "", "", "", now(), 1, true, 1, 1, false, false) val messageWithAttachment = MessageWithAttachment(testMessage, emptyList()) testObservingStrategy.isInternetConnection = false diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/mobiledevice/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/mobiledevice/MobileDeviceRepositoryTest.kt index c586572f6..adfa55eaa 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/mobiledevice/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/mobiledevice/MobileDeviceRepositoryTest.kt @@ -4,7 +4,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.repositories.UnitTestInternetObservingStrategy -import io.github.wulkanowy.getStudentEntity +import io.github.wulkanowy.data.repositories.getStudentEntity import io.reactivex.Maybe import io.reactivex.Single import org.junit.Before diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/semester/SemesterRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/semester/SemesterRepositoryTest.kt index c00a771cc..58c94ab6c 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/semester/SemesterRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/semester/SemesterRepositoryTest.kt @@ -4,7 +4,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.repositories.UnitTestInternetObservingStrategy -import io.github.wulkanowy.createSemesterEntity +import io.github.wulkanowy.data.repositories.createSemesterEntity import io.reactivex.Maybe import io.reactivex.Single import org.junit.Assert.assertEquals diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/timetable/TimetableRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/timetable/TimetableRemoteTest.kt index d2b4a6670..c4548db60 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/timetable/TimetableRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/timetable/TimetableRemoteTest.kt @@ -1,7 +1,7 @@ package io.github.wulkanowy.data.repositories.timetable import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.getStudentEntity +import io.github.wulkanowy.data.repositories.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Timetable import io.mockk.MockKAnnotations diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index 984bbbcf8..009ed610e 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -1,19 +1,19 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.createSemesterEntity import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary 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.data.repositories.createSemesterEntity import io.github.wulkanowy.sdk.Sdk import io.reactivex.Single import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.`when` +import org.mockito.Mockito.doReturn import org.mockito.MockitoAnnotations import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.of @@ -25,10 +25,10 @@ class GradeAverageProviderTest { lateinit var preferencesRepository: PreferencesRepository @Mock - lateinit var semesterRepository: SemesterRepository + lateinit var gradeRepository: GradeRepository @Mock - lateinit var gradeRepository: GradeRepository + lateinit var gradeSummaryRepository: GradeSummaryRepository private lateinit var gradeAverageProvider: GradeAverageProvider @@ -41,414 +41,175 @@ class GradeAverageProviderTest { ) private val firstGrades = listOf( - // avg: 3.5 getGrade(22, "Matematyka", 4.0), getGrade(22, "Matematyka", 3.0), - - // avg: 3.5 getGrade(22, "Fizyka", 6.0), getGrade(22, "Fizyka", 1.0) ) - private val firstSummaries = listOf( - getSummary(semesterId = 22, subject = "Matematyka", average = 3.9), - getSummary(semesterId = 22, subject = "Fizyka", average = 3.1) - ) - - private val secondGrades = listOf( - // avg: 2.5 + private val secondGrade = listOf( getGrade(23, "Matematyka", 2.0), getGrade(23, "Matematyka", 3.0), - - // avg: 3.0 getGrade(23, "Fizyka", 4.0), getGrade(23, "Fizyka", 2.0) ) - private val secondSummaries = listOf( - getSummary(semesterId = 23, subject = "Matematyka", average = 2.9), - getSummary(semesterId = 23, subject = "Fizyka", average = 3.4) - ) - private val secondGradeWithModifier = listOf( - // avg: 3.375 getGrade(24, "Język polski", 3.0, -0.50), getGrade(24, "Język polski", 4.0, 0.25) ) - private val secondSummariesWithModifier = listOf( - getSummary(24, "Język polski", 3.49) - ) - @Before - fun setUp() { + fun initTest() { MockitoAnnotations.initMocks(this) + gradeAverageProvider = GradeAverageProvider(preferencesRepository, gradeRepository, gradeSummaryRepository) - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false) - `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) - `when`(preferencesRepository.gradeMinusModifier).thenReturn(.33) - `when`(preferencesRepository.gradePlusModifier).thenReturn(.33) + doReturn(.33).`when`(preferencesRepository).gradeMinusModifier + doReturn(.33).`when`(preferencesRepository).gradePlusModifier + doReturn(false).`when`(preferencesRepository).gradeAverageForceCalc - gradeAverageProvider = GradeAverageProvider(semesterRepository, gradeRepository, preferencesRepository) + doReturn(Single.just(firstGrades)).`when`(gradeRepository).getGrades(student, semesters[1], true) + doReturn(Single.just(secondGrade)).`when`(gradeRepository).getGrades(student, semesters[2], true) } @Test - fun `force calc current semester average with default modifiers in scraper mode`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ONE_SEMESTER) - `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier)) + fun onlyOneSemesterTest() { + doReturn("only_one_semester").`when`(preferencesRepository).gradeAverageMode + doReturn(Single.just(emptyList())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[2], true) - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() + val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) + .blockingGet() - assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) // from details and after set custom plus/minus + assertEquals(2, averages.size) + assertEquals(2.5, averages.single { it.first == "Matematyka" }.second, .0) + assertEquals(3.0, averages.single { it.first == "Fizyka" }.second, .0) } @Test - fun `force calc current semester average with custom modifiers in scraper mode`() { - val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) + 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.gradeMinusModifier).thenReturn(.33) - `when`(preferencesRepository.gradePlusModifier).thenReturn(.33) + val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) + .blockingGet() - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ONE_SEMESTER) - `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier)) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) // from details and after set custom plus/minus + assertEquals(3.5, averages.single { it.first == "Język polski" }.second, .0) } @Test - fun `force calc current semester average with custom modifiers in api mode`() { - val student = student.copy(loginMode = Sdk.Mode.API.name) + 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) - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeMinusModifier).thenReturn(.33) // useless in this mode - `when`(preferencesRepository.gradePlusModifier).thenReturn(.33) + val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.API.name), semesters, semesters[2].semesterId, true) + .blockingGet() - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ONE_SEMESTER) - `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier)) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0) // (from details): 3.375 + assertEquals(3.375, averages.single { it.first == "Język polski" }.second, .0) } @Test - fun `force calc current semester average with custom modifiers in hybrid mode`() { - val student = student.copy(loginMode = Sdk.Mode.HYBRID.name) + 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) - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeMinusModifier).thenReturn(.33) // useless in this mode - `when`(preferencesRepository.gradePlusModifier).thenReturn(.33) + val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.SCRAPPER.name), semesters, semesters[2].semesterId, true) + .blockingGet() - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ONE_SEMESTER) - `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGradeWithModifier to secondSummariesWithModifier)) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0) // (from details): 3.375 + assertEquals(3.5, averages.single { it.first == "Język polski" }.second, .0) } @Test - fun `calc current semester average`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ONE_SEMESTER) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries)) + 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 items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() + val averages = gradeAverageProvider.getGradeAverage(student.copy(loginMode = Sdk.Mode.HYBRID.name), semesters, semesters[2].semesterId, true) + .blockingGet() - assertEquals(2, items.size) - assertEquals(2.9, items.single { it.subject == "Matematyka" }.average, .0) // from summary: 2,9 - assertEquals(3.4, items.single { it.subject == "Fizyka" }.average, .0) // from details: 3,4 + assertEquals(3.375, averages.single { it.first == "Język polski" }.second, .0) } @Test - fun `force calc current semester average`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ONE_SEMESTER) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries)) + fun allYearFirstSemesterTest() { + doReturn("all_year").`when`(preferencesRepository).gradeAverageMode + doReturn(Single.just(emptyList())).`when`(gradeSummaryRepository).getGradesSummary(student, semesters[1], true) - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() + val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[1].semesterId, true) + .blockingGet() - assertEquals(2, items.size) - assertEquals(2.5, items.single { it.subject == "Matematyka" }.average, .0) // from details: 2,5 - assertEquals(3.0, items.single { it.subject == "Fizyka" }.average, .0) // from details: 3,0 + assertEquals(2, averages.size) + assertEquals(3.5, averages.single { it.first == "Matematyka" }.second, .0) + assertEquals(3.5, averages.single { it.first == "Fizyka" }.second, .0) } @Test - fun `force calc full year average when current is first`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ALL_YEAR) - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries)) + 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) - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId).blockingGet() + val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) + .blockingGet() - assertEquals(2, items.size) - assertEquals(3.5, items.single { it.subject == "Matematyka" }.average, .0) // (from summary): 3,5 - assertEquals(3.5, items.single { it.subject == "Fizyka" }.average, .0) // (from summary): 3,5 + assertEquals(2, averages.size) + assertEquals(3.0, averages.single { it.first == "Matematyka" }.second, .0) + assertEquals(3.25, averages.single { it.first == "Fizyka" }.second, .0) + } + + @Test(expected = IllegalArgumentException::class) + fun incorrectAverageModeTest() { + doReturn("test_mode").`when`(preferencesRepository).gradeAverageMode + + gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true).blockingGet() } @Test - fun `calc both semesters average`() { - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.BOTH_SEMESTERS) - `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) - ))) + 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) - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() + val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) + .blockingGet() - assertEquals(2, items.size) - assertEquals(3.25, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries ↑): 3,0 + 3,5 → 3,25 - assertEquals(3.75, items.single { it.subject == "Fizyka" }.average, .0) // (from summaries ↑): 3,5 + 4,0 → 3,75 + assertEquals(2, averages.size) + assertEquals(3.1, averages.single { it.first == "Matematyka" }.second, .0) + assertEquals(3.26, averages.single { it.first == "Fizyka" }.second, .0) } @Test - fun `force calc full year average`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.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 listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) - ))) + 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) - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() + val averages = gradeAverageProvider.getGradeAverage(student, semesters, semesters[2].semesterId, true) + .blockingGet() - assertEquals(2, items.size) - assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 + assertEquals(2, averages.size) + assertEquals(3.0, averages.single { it.first == "Matematyka" }.second, .0) + assertEquals(3.25, averages.single { it.first == "Fizyka" }.second, .0) } - @Test - fun `calc both semesters average when no summaries`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.BOTH_SEMESTERS) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to emptyList())) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to emptyList())) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(2, items.size) - assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 - } - - @Test - fun `force calc full year average when no summaries`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ALL_YEAR) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to emptyList())) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to emptyList())) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(2, items.size) - assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 - } - - @Test - fun `calc both semesters average when missing summaries in both semesters`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.BOTH_SEMESTERS) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to listOf( - getSummary(22, "Matematyka", 4.0) - ))) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to listOf( - getSummary(23, "Matematyka", 3.0) - ))) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(2, items.size) - assertEquals(3.5, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries ↑): 4,0 + 3,0 → 3,5 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 - } - - @Test - fun `calc both semesters average when missing summary in second semester`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.BOTH_SEMESTERS) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries)) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries.dropLast(1))) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(2, items.size) - assertEquals(3.4, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries): 3,9 + 2,9 → 3,4 - assertEquals(3.05, items.single { it.subject == "Fizyka" }.average, .0) // 3,1 (from summary) + 3,0 (from details) → 3,05 - } - - @Test - fun `calc both semesters average when missing summary in first semester`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(false) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.BOTH_SEMESTERS) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries.dropLast(1))) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries)) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(2, items.size) - assertEquals(3.4, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries): 3,9 + 2,9 → 3,4 - assertEquals(3.45, items.single { it.subject == "Fizyka" }.average, .0) // 3,5 (from details) + 3,4 (from summary) → 3,45 - } - - @Test - fun `force calc full year average when missing summary in first semester`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ALL_YEAR) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(firstGrades to firstSummaries.dropLast(1))) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(secondGrades to secondSummaries)) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(2, items.size) - assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 - } - - @Test - fun `force calc both semesters average with different average from all grades and from two semesters`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.BOTH_SEMESTERS) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)))) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)))) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(5.2296, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,732 → 5.229636363636364 - } - - @Test - fun `force calc full year average with different average from all grades and from two semesters`() { - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ALL_YEAR) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)))) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)))) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(5.5429, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,732 → .average() - } - - @Test - fun `force calc both semesters average with different average from all grades and from two semesters with custom modifiers`() { - val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeMinusModifier).thenReturn(.33) - `when`(preferencesRepository.gradePlusModifier).thenReturn(.5) - - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.BOTH_SEMESTERS) - `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)))) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)))) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(5.2636, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,8 → 5.26363636 - } - - @Test - fun `force calc full year average with different average from all grades and from two semesters with custom modifiers`() { - val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - - `when`(preferencesRepository.gradeAverageForceCalc).thenReturn(true) - `when`(preferencesRepository.gradeMinusModifier).thenReturn(.33) - `when`(preferencesRepository.gradePlusModifier).thenReturn(.5) - - `when`(preferencesRepository.gradeAverageMode).thenReturn(GradeAverageMode.ALL_YEAR) - `when`(semesterRepository.getSemesters(student)).thenReturn(Single.just(semesters)) - - `when`(gradeRepository.getGrades(student, semesters[1])).thenReturn(Single.just(listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)))) - `when`(gradeRepository.getGrades(student, semesters[2])).thenReturn(Single.just(listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)))) - - val items = gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId).blockingGet() - - assertEquals(5.5555, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,8 → .average() - } - - private fun getGrade(semesterId: Int, subject: String, value: Double, modifier: Double = 0.0, weight: Double = 1.0): Grade { + private fun getGrade(semesterId: Int, subject: String, value: Double, modifier: Double = 0.0): Grade { return Grade( studentId = 101, semesterId = semesterId, subject = subject, value = value, modifier = modifier, - weightValue = weight, + weightValue = 1.0, teacher = "", date = now(), weight = "", @@ -460,12 +221,12 @@ class GradeAverageProviderTest { ) } - private fun getSummary(semesterId: Int, subject: String, average: Double): GradeSummary { + private fun getSummary(semesterId: Int, subject: String, value: Double): GradeSummary { return GradeSummary( studentId = 101, semesterId = semesterId, subject = subject, - average = average, + average = value, 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 121391dee..258dc54fe 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(testStudent, false) + presenter.onItemSelected(LoginStudentSelectItem(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(testStudent, false) + presenter.onItemSelected(LoginStudentSelectItem(testStudent, false)) presenter.onSignIn() verify(loginStudentSelectView).showContent(false) verify(loginStudentSelectView).showProgress(true) diff --git a/app/src/test/java/io/github/wulkanowy/utils/TimeExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/TimeExtensionTest.kt index 72d08c411..024b4727b 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/TimeExtensionTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/TimeExtensionTest.kt @@ -38,12 +38,12 @@ class TimeExtensionTest { } @Test - fun sundayTestTest() { - assertEquals(of(2018, 10, 7), of(2018, 10, 2).sunday) - assertEquals(of(2018, 10, 7), of(2018, 10, 5).sunday) - assertEquals(of(2018, 10, 7), of(2018, 10, 6).sunday) - assertEquals(of(2018, 10, 7), of(2018, 10, 7).sunday) - assertEquals(of(2018, 10, 14), of(2018, 10, 8).sunday) + fun fridayTest() { + assertEquals(of(2018, 10, 5), of(2018, 10, 2).friday) + assertEquals(of(2018, 10, 5), of(2018, 10, 5).friday) + assertEquals(of(2018, 10, 5), of(2018, 10, 6).friday) + assertEquals(of(2018, 10, 5), of(2018, 10, 7).friday) + assertEquals(of(2018, 10, 12), of(2018, 10, 8).friday) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt deleted file mode 100644 index 33a798502..000000000 --- a/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -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 4de7310b5..658cb977e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,21 @@ buildscript { ext.kotlin_version = '1.3.72' - ext.about_libraries = '8.2.0' + ext.about_libraries = '8.1.2' repositories { mavenCentral() 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:4.0.0' + classpath 'com.android.tools.build:gradle:3.6.3' classpath 'com.google.gms:google-services:4.3.3' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.1' - classpath "com.github.triplet.gradle:play-publisher:2.7.5" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.0" + //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 "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 deleted file mode 100644 index 2399a56b5..000000000 --- a/crowdin.yml +++ /dev/null @@ -1,14 +0,0 @@ -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 6623300be..84a906615 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.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists