diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d744bdd14..a81b333f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,39 +10,14 @@ on: workflow_dispatch: jobs: - build: - name: Pre-build - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: fkirc/skip-duplicate-actions@master - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v1 - with: - java-version: 11 - - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} - - name: Build - run: ./gradlew --build-cache compileFdroidDebugUnitTestKotlin preFdroidDebugAndroidTestBuild dexBuilderFdroidDebugAndroidTest packageFdroidDebug packageFdroidDebugAndroidTest - - name: Prepare build cache - run: tar -cf prebuild.tar .build-cache .gradle app/build - - uses: actions/upload-artifact@v2 - with: - name: prebuild.tar - path: prebuild.tar - unit-tests: name: Unit tests runs-on: ubuntu-latest timeout-minutes: 10 - needs: [ build ] steps: + - uses: fkirc/skip-duplicate-actions@master - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 - uses: actions/setup-java@v1 with: java-version: 11 @@ -52,11 +27,6 @@ jobs: ~/.gradle/caches ~/.gradle/wrapper key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} - - uses: actions/download-artifact@v2 - with: - name: prebuild.tar - - name: Extract build cache - run: tar -xf prebuild.tar - name: Unit tests run: | ./gradlew --build-cache -Pcoverage testFdroidDebugUnitTest --stacktrace @@ -65,49 +35,12 @@ jobs: with: flags: unit - instrumentation-tests: - name: Instrumentation tests - runs-on: macOS-latest - timeout-minutes: 15 - needs: [ build ] - strategy: - fail-fast: true - matrix: - api-level: [21, 29] - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 - with: - java-version: 11 - - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} - - uses: actions/download-artifact@v2 - with: - name: prebuild.tar - - name: Extract build cache - run: tar -xf prebuild.tar - - name: Instrumentation tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - arch: x86 - script: | - ./gradlew --build-cache -Pcoverage connectedFdroidDebugAndroidTest --stacktrace - ./gradlew --build-cache -Pcoverage jacocoTestReport --stacktrace - - uses: codecov/codecov-action@v1 - with: - flags: instrumented,api-${{ matrix.api-level }} - deploy-google-play: name: Deploy to google play runs-on: ubuntu-latest timeout-minutes: 10 environment: google-play - needs: [ build, unit-tests, instrumentation-tests ] + needs: [ unit-tests ] if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') steps: - uses: actions/checkout@v2 @@ -120,11 +53,6 @@ jobs: ~/.gradle/caches ~/.gradle/wrapper key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} - - uses: actions/download-artifact@v2 - with: - name: prebuild.tar - - name: Extract build cache - run: tar -xf prebuild.tar - name: Decrypt keys env: ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }} @@ -140,3 +68,135 @@ jobs: PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }} PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace; + + deploy-appcenter: + name: Deploy to App Center + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: app-center + if: github.ref != 'refs/heads/develop' + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 11 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} + - name: Set run number with offset + env: + BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }} + run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV + - name: Prepare build configuration + run: | + sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle + sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/google-services.json + sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/agconnect-services.json + sed -i -e '/versionNameSuffix/d' app/build.gradle + - name: Add signing config + run: | + cat >> app/build.gradle <> $GITHUB_ENV + - name: Add signing config + run: | + cat >> app/build.gradle <; -} +-keep class com.google.android.material.tabs.** { *; } \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/35.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/35.json new file mode 100644 index 000000000..f166b2d72 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/35.json @@ -0,0 +1,2148 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "identityHash": "15fb91ec180fe60bf400cfa729c3418d", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER 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, `user_name` TEXT 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": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "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": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "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": "GradePartialStatistics", + "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, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT 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": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "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": "GradeSemesterStatistics", + "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, `amounts` TEXT NOT NULL, `student_grade` 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": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "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, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT 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, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` 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": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "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": "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": "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": "unitId", + "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": "userLoginId", + "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": "userLoginId", + "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": [] + }, + { + "tableName": "Conferences", + "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, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER 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": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "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, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` 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": "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "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, '15fb91ec180fe60bf400cfa729c3418d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac8d3be4a..7b714fb26 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -56,7 +56,7 @@ android:name=".ui.modules.login.LoginActivity" android:configChanges="orientation|screenSize" android:label="@string/login_title" - android:theme="@style/WulkanowyTheme.NoActionBar" + android:theme="@style/WulkanowyTheme.Login" android:windowSoftInputMode="adjustResize" /> { - return arrayOf( - Migration2(), - Migration3(), - Migration4(), - Migration5(), - Migration6(), - Migration7(), - Migration8(), - Migration9(), - Migration10(), - Migration11(), - Migration12(), - Migration13(), - Migration14(), - Migration15(), - Migration16(), - Migration17(), - Migration18(), - Migration19(sharedPrefProvider), - Migration20(), - Migration21(), - Migration22(), - Migration23(), - Migration24(), - Migration25(), - Migration26(), - Migration27(), - Migration28(), - Migration29(), - Migration30(), - Migration31(), - Migration32(), - Migration33(), - Migration34() - ) - } + fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( + Migration2(), + Migration3(), + Migration4(), + Migration5(), + Migration6(), + Migration7(), + Migration8(), + Migration9(), + Migration10(), + Migration11(), + Migration12(), + Migration13(), + Migration14(), + Migration15(), + Migration16(), + Migration17(), + Migration18(), + Migration19(sharedPrefProvider), + Migration20(), + Migration21(), + Migration22(), + Migration23(), + Migration24(), + Migration25(), + Migration26(), + Migration27(), + Migration28(), + Migration29(), + Migration30(), + Migration31(), + Migration32(), + Migration33(), + Migration34(), + Migration35(appInfo) + ) - fun newInstance(context: Context, sharedPrefProvider: SharedPrefProvider): AppDatabase { - return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database") - .setJournalMode(TRUNCATE) - .fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1) - .fallbackToDestructiveMigrationOnDowngrade() - .addMigrations(*getMigrations(sharedPrefProvider)) - .build() - } + fun newInstance( + context: Context, + sharedPrefProvider: SharedPrefProvider, + appInfo: AppInfo + ) = Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database") + .setJournalMode(TRUNCATE) + .fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1) + .fallbackToDestructiveMigrationOnDowngrade() + .addMigrations(*getMigrations(sharedPrefProvider, appInfo)) + .build() } abstract val studentDao: StudentDao diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/LuckyNumberDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/LuckyNumberDao.kt index 57f3005ad..d9aa24364 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/LuckyNumberDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/LuckyNumberDao.kt @@ -13,4 +13,7 @@ interface LuckyNumberDao : BaseDao { @Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date = :date") fun load(studentId: Int, date: LocalDate): Flow + + @Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date >= :start AND date <= :end") + fun getAll(studentId: Int, start: LocalDate, end: LocalDate): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index e9c5f157e..0ad2ee590 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -8,7 +8,7 @@ import androidx.room.Query import androidx.room.Transaction import androidx.room.Update import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.StudentNick +import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import javax.inject.Singleton @@ -23,13 +23,13 @@ interface StudentDao { suspend fun delete(student: Student) @Update(entity = Student::class) - suspend fun update(studentNick: StudentNick) + suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) @Query("SELECT * FROM Students WHERE is_current = 1") suspend fun loadCurrent(): Student? @Query("SELECT * FROM Students WHERE id = :id") - suspend fun loadById(id: Int): Student? + suspend fun loadById(id: Long): Student? @Query("SELECT * FROM Students") suspend fun loadAll(): List diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt index 1f10a1645..7b6e0dbf2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt @@ -10,7 +10,7 @@ import java.time.LocalDateTime data class Message( @ColumnInfo(name = "student_id") - val studentId: Int, + val studentId: Long, @ColumnInfo(name = "real_id") val realId: Int, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt index 6b60c8146..af9fe831a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt @@ -81,4 +81,7 @@ data class Student( var id: Long = 0 var nick = "" + + @ColumnInfo(name = "avatar_color") + var avatarColor = 0L } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNick.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNickAndAvatar.kt similarity index 57% rename from app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNick.kt rename to app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNickAndAvatar.kt index 71f48f7a3..546059eea 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNick.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentNickAndAvatar.kt @@ -1,13 +1,17 @@ package io.github.wulkanowy.data.db.entities +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.io.Serializable @Entity -data class StudentNick( +data class StudentNickAndAvatar( - val nick: String + val nick: String, + + @ColumnInfo(name = "avatar_color") + var avatarColor: Long ) : Serializable { diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt new file mode 100644 index 000000000..cc540388c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.core.database.getLongOrNull +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import io.github.wulkanowy.utils.AppInfo + +class Migration35(private val appInfo: AppInfo) : Migration(34, 35) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Students ADD COLUMN `avatar_color` INTEGER NOT NULL DEFAULT 0") + + val studentsCursor = database.query("SELECT * FROM Students") + + while (studentsCursor.moveToNext()) { + val studentId = studentsCursor.getLongOrNull(0) + database.execSQL( + """UPDATE Students + SET avatar_color = ${appInfo.defaultColorsForAvatar.random()} + WHERE id = $studentId""" + ) + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeStatisticsMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeStatisticsMapper.kt index fbd40433c..b25802d22 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeStatisticsMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeStatisticsMapper.kt @@ -5,10 +5,9 @@ import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.pojos.GradeStatisticsItem -import io.github.wulkanowy.ui.modules.grade.statistics.ViewType -import io.github.wulkanowy.sdk.pojo.GradeStatisticsSubject as SdkGradeStatisticsSubject -import io.github.wulkanowy.sdk.pojo.GradeStatisticsSemester as SdkGradeStatisticsSemester import io.github.wulkanowy.sdk.pojo.GradePointsStatistics as SdkGradePointsStatistics +import io.github.wulkanowy.sdk.pojo.GradeStatisticsSemester as SdkGradeStatisticsSemester +import io.github.wulkanowy.sdk.pojo.GradeStatisticsSubject as SdkGradeStatisticsSubject @JvmName("mapToEntitiesSubject") fun List.mapToEntities(semester: Semester) = map { @@ -51,7 +50,7 @@ fun List.mapToEntities(semester: Semester) = map { fun List.mapPartialToStatisticItems() = filterNot { it.classAmounts.isEmpty() }.map { GradeStatisticsItem( - type = ViewType.PARTIAL, + type = GradeStatisticsItem.DataType.PARTIAL, average = it.classAverage, partial = it, points = null, @@ -61,7 +60,7 @@ fun List.mapPartialToStatisticItems() = filterNot { it.c fun List.mapSemesterToStatisticItems() = filterNot { it.amounts.isEmpty() }.map { GradeStatisticsItem( - type = ViewType.SEMESTER, + type = GradeStatisticsItem.DataType.SEMESTER, partial = null, points = null, average = "", @@ -71,7 +70,7 @@ fun List.mapSemesterToStatisticItems() = filterNot { it fun List.mapPointsToStatisticsItems() = map { GradeStatisticsItem( - type = ViewType.POINTS, + type = GradeStatisticsItem.DataType.POINTS, partial = null, semester = null, average = "", diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt index 2c815b305..913e4d030 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt @@ -4,14 +4,14 @@ import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient -import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment import java.time.LocalDateTime import io.github.wulkanowy.sdk.pojo.Message as SdkMessage +import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment +import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient fun List.mapToEntities(student: Student) = map { Message( - studentId = student.id.toInt(), + studentId = student.id, realId = it.id ?: 0, messageId = it.messageId ?: 0, sender = it.sender?.name.orEmpty(), diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt index 67f56c628..c93323038 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt @@ -5,7 +5,7 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters import java.time.LocalDateTime import io.github.wulkanowy.sdk.pojo.Student as SdkStudent -fun List.mapToEntities(password: String = "") = map { +fun List.mapToEntities(password: String = "", colors: List) = map { StudentWithSemesters( student = Student( email = it.email, @@ -28,8 +28,10 @@ fun List.mapToEntities(password: String = "") = map { mobileBaseUrl = it.mobileBaseUrl, privateKey = it.privateKey, certificateKey = it.certificateKey, - loginMode = it.loginMode.name - ), + loginMode = it.loginMode.name, + ).apply { + avatarColor = colors.random() + }, semesters = it.semesters.mapToEntities(it.studentId) ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/GradeStatisticsItem.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/GradeStatisticsItem.kt index 88257470d..bdcd049df 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/GradeStatisticsItem.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/GradeStatisticsItem.kt @@ -3,11 +3,10 @@ package io.github.wulkanowy.data.pojos import io.github.wulkanowy.data.db.entities.GradePartialStatistics import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics -import io.github.wulkanowy.ui.modules.grade.statistics.ViewType data class GradeStatisticsItem( - val type: ViewType, + val type: DataType, val average: String, @@ -16,4 +15,11 @@ data class GradeStatisticsItem( val semester: GradeSemesterStatistics?, val points: GradePointsStatistics? -) + +) { + enum class DataType { + SEMESTER, + PARTIAL, + POINTS, + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index 9a6528f39..ffccb059e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -27,9 +28,12 @@ class AttendanceRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "attendance" fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index 4edb507be..cd4403c7d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -20,9 +21,12 @@ class AttendanceSummaryRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "attendance_summary" fun getAttendanceSummary(student: Student, semester: Semester, subjectId: Int, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt index 59aabdd54..99ef56f4b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt @@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -23,9 +24,12 @@ class CompletedLessonsRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "completed" fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index befcf9e6b..0a839d27b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -20,9 +21,12 @@ class ConferenceRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "conference" fun getConferences(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, query = { conferenceDb.loadAll(semester.diaryId, student.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index bd6e7d2d6..a8912f100 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.startExamsDay import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -23,9 +24,12 @@ class ExamRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "exam" fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { examDb.loadAll(semester.diaryId, semester.studentId, start.startExamsDay, start.endExamsDay) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index bab290f32..9880e4641 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -16,6 +16,7 @@ import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import java.time.LocalDateTime import javax.inject.Inject import javax.inject.Singleton @@ -28,14 +29,20 @@ class GradeRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "grade" fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( - shouldFetch = { (details, summaries) -> details.isEmpty() || summaries.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, + mutex = saveFetchResultMutex, + shouldFetch = { (details, summaries) -> + val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed + }, query = { - gradeDb.loadAll(semester.semesterId, semester.studentId).combine(gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)) { details, summaries -> - details to summaries - } + val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) + val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) + detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries } }, fetch = { val (details, summary) = sdk.init(student) @@ -92,19 +99,27 @@ class GradeRepository @Inject constructor( } fun getUnreadGrades(semester: Semester): Flow> { - return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isRead } } + return gradeDb.loadAll(semester.semesterId, semester.studentId).map { + it.filter { grade -> !grade.isRead } + } } fun getNotNotifiedGrades(semester: Semester): Flow> { - return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isNotified } } + return gradeDb.loadAll(semester.semesterId, semester.studentId).map { + it.filter { grade -> !grade.isNotified } + } } fun getNotNotifiedPredictedGrades(semester: Semester): Flow> { - return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } } + return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { + it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } + } } fun getNotNotifiedFinalGrades(semester: Semester): Flow> { - return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } } + return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { + it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } + } } suspend fun updateGrade(grade: Grade) { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt index ab65fb141..9cd8e711d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt @@ -17,6 +17,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -30,11 +31,16 @@ class GradeStatisticsRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val partialMutex = Mutex() + private val semesterMutex = Mutex() + private val pointsMutex = Mutex() + private val partialCacheKey = "grade_stats_partial" private val semesterCacheKey = "grade_stats_semester" private val pointsCacheKey = "grade_stats_points" fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + mutex = partialMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { @@ -71,6 +77,7 @@ class GradeStatisticsRepository @Inject constructor( ) fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + mutex = semesterMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { @@ -112,6 +119,7 @@ class GradeStatisticsRepository @Inject constructor( ) fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + mutex = pointsMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index 7625dbbc8..068fd9a5c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -24,9 +25,12 @@ class HomeworkRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "homework" fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 801292b42..b904b7dba 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -9,6 +9,8 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import java.time.LocalDate import java.time.LocalDate.now import javax.inject.Inject import javax.inject.Singleton @@ -19,7 +21,10 @@ class LuckyNumberRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { luckyNumberDb.load(student.studentId, now()) }, fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, @@ -33,6 +38,9 @@ class LuckyNumberRepository @Inject constructor( } ) + fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) = + luckyNumberDb.getAll(student.studentId, start, end) + suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map { if (it?.isNotified == false) it else null }.first() diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index ea7b2b0e0..5f5554187 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -20,6 +20,7 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import timber.log.Timber import java.time.LocalDateTime.now import javax.inject.Inject @@ -33,10 +34,13 @@ class MessageRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "message" @Suppress("UNUSED_PARAMETER") fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt index 7e83ef7d5..4b333bc6d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -23,9 +24,12 @@ class MobileDeviceRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "devices" fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) }, query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index 85789f098..85339dfa9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -23,9 +24,12 @@ class NoteRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "note" fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, query = { noteDb.loadAll(student.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 8cb815cc3..11adbd0f7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -18,26 +18,43 @@ class PreferencesRepository @Inject constructor( get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() val isShowPresent: Boolean - get() = getBoolean(R.string.pref_key_attendance_present, R.bool.pref_default_attendance_present) + get() = getBoolean( + R.string.pref_key_attendance_present, + R.bool.pref_default_attendance_present + ) val gradeAverageMode: GradeAverageMode - get() = GradeAverageMode.getByValue(getString(R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode)) + get() = GradeAverageMode.getByValue( + getString( + R.string.pref_key_grade_average_mode, + R.string.pref_default_grade_average_mode + ) + ) val gradeAverageForceCalc: Boolean - get() = getBoolean(R.string.pref_key_grade_average_force_calc, R.bool.pref_default_grade_average_force_calc) + get() = getBoolean( + R.string.pref_key_grade_average_force_calc, + R.bool.pref_default_grade_average_force_calc + ) val isGradeExpandable: Boolean get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade) val showAllSubjectsOnStatisticsList: Boolean - get() = getBoolean(R.string.pref_key_grade_statistics_list, R.bool.pref_default_grade_statistics_list) + get() = getBoolean( + R.string.pref_key_grade_statistics_list, + R.bool.pref_default_grade_statistics_list + ) val appThemeKey = context.getString(R.string.pref_key_app_theme) val appTheme: String get() = getString(appThemeKey, R.string.pref_default_app_theme) val gradeColorTheme: String - get() = getString(R.string.pref_key_grade_color_scheme, R.string.pref_default_grade_color_scheme) + get() = getString( + R.string.pref_key_grade_color_scheme, + R.string.pref_default_grade_color_scheme + ) val appLanguageKey = context.getString(R.string.pref_key_app_language) val appLanguage @@ -55,50 +72,86 @@ class PreferencesRepository @Inject constructor( val isServicesOnlyWifi: Boolean get() = getBoolean(servicesOnlyWifiKey, R.bool.pref_default_services_wifi_only) + val notificationsEnableKey = context.getString(R.string.pref_key_notifications_enable) val isNotificationsEnable: Boolean - get() = getBoolean(R.string.pref_key_notifications_enable, R.bool.pref_default_notifications_enable) + get() = getBoolean(notificationsEnableKey, R.bool.pref_default_notifications_enable) - val isUpcomingLessonsNotificationsEnableKey = context.getString(R.string.pref_key_notifications_upcoming_lessons_enable) + val isUpcomingLessonsNotificationsEnableKey = + context.getString(R.string.pref_key_notifications_upcoming_lessons_enable) val isUpcomingLessonsNotificationsEnable: Boolean - get() = getBoolean(isUpcomingLessonsNotificationsEnableKey, R.bool.pref_default_notification_upcoming_lessons_enable) + get() = getBoolean( + isUpcomingLessonsNotificationsEnableKey, + R.bool.pref_default_notification_upcoming_lessons_enable + ) val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug) val isDebugNotificationEnable: Boolean get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug) val gradePlusModifier: Double - get() = getString(R.string.pref_key_grade_modifier_plus, R.string.pref_default_grade_modifier_plus).toDouble() + get() = getString( + R.string.pref_key_grade_modifier_plus, + R.string.pref_default_grade_modifier_plus + ).toDouble() val gradeMinusModifier: Double - get() = getString(R.string.pref_key_grade_modifier_minus, R.string.pref_default_grade_modifier_minus).toDouble() + get() = getString( + R.string.pref_key_grade_modifier_minus, + R.string.pref_default_grade_modifier_minus + ).toDouble() val fillMessageContent: Boolean - get() = getBoolean(R.string.pref_key_fill_message_content, R.bool.pref_default_fill_message_content) + get() = getBoolean( + R.string.pref_key_fill_message_content, + R.bool.pref_default_fill_message_content + ) val showGroupsInPlan: Boolean - get() = getBoolean(R.string.pref_key_timetable_show_groups, R.bool.pref_default_timetable_show_groups) + get() = getBoolean( + R.string.pref_key_timetable_show_groups, + R.bool.pref_default_timetable_show_groups + ) val showWholeClassPlan: String - get() = getString(R.string.pref_key_timetable_show_whole_class, R.string.pref_default_timetable_show_whole_class) + get() = getString( + R.string.pref_key_timetable_show_whole_class, + R.string.pref_default_timetable_show_whole_class + ) val gradeSortingMode: GradeSortingMode - get() = GradeSortingMode.getByValue(getString(R.string.pref_key_grade_sorting_mode, R.string.pref_default_grade_sorting_mode)) + get() = GradeSortingMode.getByValue( + getString( + R.string.pref_key_grade_sorting_mode, + R.string.pref_default_grade_sorting_mode + ) + ) val showTimetableTimers: Boolean - get() = getBoolean(R.string.pref_key_timetable_show_timers, R.bool.pref_default_timetable_show_timers) + get() = getBoolean( + R.string.pref_key_timetable_show_timers, + R.bool.pref_default_timetable_show_timers + ) var isHomeworkFullscreen: Boolean - get() = getBoolean(R.string.pref_key_homework_fullscreen, R.bool.pref_default_homework_fullscreen) + get() = getBoolean( + R.string.pref_key_homework_fullscreen, + R.bool.pref_default_homework_fullscreen + ) set(value) = sharedPref.edit().putBoolean("homework_fullscreen", value).apply() val showSubjectsWithoutGrades: Boolean - get() = getBoolean(R.string.pref_key_subjects_without_grades, R.bool.pref_default_subjects_without_grades) + get() = getBoolean( + R.string.pref_key_subjects_without_grades, + R.bool.pref_default_subjects_without_grades + ) private fun getString(id: Int, default: Int) = getString(context.getString(id), default) - private fun getString(id: String, default: Int) = sharedPref.getString(id, context.getString(default)) ?: context.getString(default) + private fun getString(id: String, default: Int) = + sharedPref.getString(id, context.getString(default)) ?: context.getString(default) private fun getBoolean(id: Int, default: Int) = getBoolean(context.getString(id), default) - private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default)) + private fun getBoolean(id: String, default: Int) = + sharedPref.getBoolean(id, context.resources.getBoolean(default)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt index 6b22b32c3..8b59cb589 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt @@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -16,8 +17,11 @@ class SchoolRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { schoolDb.load(semester.studentId, semester.classId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt index e3deb447f..de66ad20f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt @@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -16,8 +17,11 @@ class StudentInfoRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { studentInfoDao.loadStudentInfo(student.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 558214799..c2f364b3d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -5,11 +5,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.StudentNick +import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.encrypt @@ -23,7 +24,8 @@ class StudentRepository @Inject constructor( private val dispatchers: DispatchersProvider, private val studentDb: StudentDao, private val semesterDb: SemesterDao, - private val sdk: Sdk + private val sdk: Sdk, + private val appInfo: AppInfo ) { suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() @@ -35,7 +37,8 @@ class StudentRepository @Inject constructor( symbol: String, token: String ): List = - sdk.getStudentsFromMobileApi(token, pin, symbol, "").mapToEntities() + sdk.getStudentsFromMobileApi(token, pin, symbol, "") + .mapToEntities(colors = appInfo.defaultColorsForAvatar) suspend fun getStudentsScrapper( email: String, @@ -44,7 +47,7 @@ class StudentRepository @Inject constructor( symbol: String ): List = sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) - .mapToEntities(password) + .mapToEntities(password, appInfo.defaultColorsForAvatar) suspend fun getStudentsHybrid( email: String, @@ -52,46 +55,58 @@ class StudentRepository @Inject constructor( scrapperBaseUrl: String, symbol: String ): List = - sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).mapToEntities(password) + sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) + .mapToEntities(password, appInfo.defaultColorsForAvatar) suspend fun getSavedStudents(decryptPass: Boolean = true) = - withContext(dispatchers.backgroundThread) { - studentDb.loadStudentsWithSemesters().map { + studentDb.loadStudentsWithSemesters() + .map { it.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = decrypt(student.password) + student.password = withContext(dispatchers.backgroundThread) { + decrypt(student.password) + } } } } - } - suspend fun getStudentById(id: Int) = withContext(dispatchers.backgroundThread) { - studentDb.loadById(id)?.apply { - if (Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) { - password = decrypt(password) + suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student { + val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() + + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + student.password = withContext(dispatchers.backgroundThread) { + decrypt(student.password) } } - } ?: throw NoCurrentStudentException() + return student + } - suspend fun getCurrentStudent(decryptPass: Boolean = true) = - withContext(dispatchers.backgroundThread) { - studentDb.loadCurrent()?.apply { - if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) { - password = decrypt(password) - } + suspend fun getCurrentStudent(decryptPass: Boolean = true): Student { + val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() + + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + student.password = withContext(dispatchers.backgroundThread) { + decrypt(student.password) } - } ?: throw NoCurrentStudentException() + } + return student + } suspend fun saveStudents(studentsWithSemesters: List): List { - semesterDb.insertSemesters(studentsWithSemesters.flatMap { it.semesters }) + val semesters = studentsWithSemesters.flatMap { it.semesters } + val students = studentsWithSemesters.map { it.student } + .map { + it.apply { + if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { + password = withContext(dispatchers.backgroundThread) { + encrypt(password, context) + } + } + } + } - return withContext(dispatchers.backgroundThread) { - studentDb.insertAll(studentsWithSemesters.map { it.student }.map { - if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { - it.copy(password = encrypt(it.password, context)) - } else it - }) - } + semesterDb.insertSemesters(semesters) + return studentDb.insertAll(students) } suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) { @@ -103,5 +118,6 @@ class StudentRepository @Inject constructor( suspend fun logoutStudent(student: Student) = studentDb.delete(student) - suspend fun updateStudentNick(studentNick: StudentNick) = studentDb.update(studentNick) + suspend fun updateStudentNickAndAvatar(studentNickAndAvatar: StudentNickAndAvatar) = + studentDb.update(studentNickAndAvatar) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt index ef07a1d47..b4bfef188 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -17,7 +18,10 @@ class SubjectRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 25da718ca..7135edbe9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -17,7 +18,10 @@ class TeacherRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh }, query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index fa1898f5b..927565b53 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -18,6 +18,7 @@ import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -31,9 +32,12 @@ class TimetableRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "timetable" fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { timetableDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) 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 deleted file mode 100644 index e69de29bb..000000000 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 86b6701b3..9b93953d4 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 @@ -37,6 +37,7 @@ abstract class BaseActivity, VB : ViewBinding> : abstract var presenter: T override fun onCreate(savedInstanceState: Bundle?) { + inject() themeManager.applyActivityTheme(this) super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) @@ -44,7 +45,9 @@ abstract class BaseActivity, VB : ViewBinding> : if (SDK_INT >= LOLLIPOP) { @Suppress("DEPRECATION") - setTaskDescription(ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))) + setTaskDescription( + ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface)) + ) } } @@ -84,4 +87,9 @@ abstract class BaseActivity, VB : ViewBinding> : invalidateOptionsMenu() presenter.onDetachView() } + + //https://github.com/google/dagger/releases/tag/dagger-2.33 + protected open fun inject() { + throw UnsupportedOperationException() + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt index 5f8bf4178..bd735535d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt @@ -4,6 +4,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter +//TODO Use ViewPager2 class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { 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 f76614e1e..4ce977709 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 @@ -42,10 +42,8 @@ class ErrorDialog : BaseDialogFragment() { companion object { private const val ARGUMENT_KEY = "Data" - fun newInstance(error: Throwable): ErrorDialog { - return ErrorDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) } - } + fun newInstance(error: Throwable) = ErrorDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) } } } @@ -57,12 +55,14 @@ class ErrorDialog : BaseDialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return DialogErrorBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogErrorBinding.inflate(inflater).apply { binding = this }.root - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) val stringWriter = StringWriter().apply { error.printStackTrace(PrintWriter(this)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt index a2379c3e7..b560ed2e7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt @@ -8,6 +8,9 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES import io.github.wulkanowy.R import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.ui.modules.login.LoginActivity +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity import javax.inject.Inject import javax.inject.Singleton @@ -17,7 +20,13 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer fun applyActivityTheme(activity: AppCompatActivity) { if (isThemeApplicable(activity)) { applyDefaultTheme() - if (preferencesRepository.appTheme == "black") activity.setTheme(R.style.WulkanowyTheme_Black) + if (preferencesRepository.appTheme == "black") { + when (activity) { + is MainActivity -> activity.setTheme(R.style.WulkanowyTheme_Black) + is LoginActivity -> activity.setTheme(R.style.WulkanowyTheme_Login_Black) + is SendMessageActivity -> activity.setTheme(R.style.WulkanowyTheme_MessageSend_Black) + } + } } } @@ -33,8 +42,13 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer } private fun isThemeApplicable(activity: AppCompatActivity): Boolean { - return activity.packageManager.getPackageInfo(activity.packageName, GET_ACTIVITIES) - .activities.singleOrNull { it.name == activity::class.java.canonicalName }?.theme - .let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar } + return activity.packageManager + .getPackageInfo(activity.packageName, GET_ACTIVITIES) + .activities.singleOrNull { it.name == activity::class.java.canonicalName } + ?.theme.let { + it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar + || it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black + || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black + } } } 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 cdd29b41b..4c3b608aa 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 @@ -4,9 +4,9 @@ import android.os.Bundle import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import androidx.appcompat.app.AlertDialog import androidx.core.text.parseAsHtml import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.entity.Library import dagger.hilt.android.AndroidEntryPoint @@ -26,14 +26,9 @@ class LicenseFragment : BaseFragment(R.layout.fragment_l @Inject lateinit var licenseAdapter: LicenseAdapter - private val libs by lazy { Libs(requireContext()) } - override val titleStringId get() = R.string.license_title - override val appLibraries: ArrayList? - get() = context?.let { - libs.prepareLibraries(it, emptyArray(), emptyArray(), autoDetect = true, checkCachedDetection = true, sort = true) - } + override val appLibraries by lazy { Libs(requireContext()).libraries } companion object { fun newInstance() = LicenseFragment() @@ -63,7 +58,7 @@ class LicenseFragment : BaseFragment(R.layout.fragment_l override fun openLicense(licenseHtml: String) { context?.let { - AlertDialog.Builder(it).apply { + MaterialAlertDialogBuilder(it).apply { setTitle(R.string.license_dialog_title) setMessage(licenseHtml.parseAsHtml()) setPositiveButton(android.R.string.ok) { _, _ -> } 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..6c97d8759 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 @@ -5,7 +5,7 @@ import io.github.wulkanowy.ui.base.BaseView interface LicenseView : BaseView { - val appLibraries: ArrayList? + val appLibraries: List fun initView() 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 index 342fd2d4b..227f06613 100644 --- 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 @@ -1,16 +1,17 @@ package io.github.wulkanowy.ui.modules.account import android.annotation.SuppressLint -import android.graphics.PorterDuff import android.view.LayoutInflater import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.HeaderAccountBinding import io.github.wulkanowy.databinding.ItemAccountBinding +import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.nickOrName import javax.inject.Inject @@ -72,9 +73,13 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter(R.layout.fragment_a override val titleStringId = R.string.account_title - override var subtitleString = "" - - override val isViewEmpty get() = accountAdapter.items.isEmpty() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } + @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding = FragmentAccountBinding.bind(view) presenter.onAttachView(this) } override fun initView() { - binding.accountErrorRetry.setOnClickListener { presenter.onRetry() } - binding.accountErrorDetails.setOnClickListener { presenter.onDetailsClick() } - binding.accountRecycler.apply { layoutManager = LinearLayoutManager(context) adapter = accountAdapter @@ -60,9 +55,7 @@ class AccountFragment : BaseFragment(R.layout.fragment_a accountAdapter.onClickListener = presenter::onItemSelected - with(binding) { - accountAdd.setOnClickListener { presenter.onAddSelected() } - } + binding.accountAdd.setOnClickListener { presenter.onAddSelected() } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -84,28 +77,7 @@ class AccountFragment : BaseFragment(R.layout.fragment_a override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) { (activity as? MainActivity)?.pushView( - AccountDetailsFragment.newInstance( - studentWithSemesters - ) + AccountDetailsFragment.newInstance(studentWithSemesters) ) } - - override fun showErrorView(show: Boolean) { - binding.accountError.visibility = if (show) View.VISIBLE else View.GONE - } - - override fun setErrorDetails(message: String) { - binding.accountErrorMessage.text = message - } - - override fun showProgress(show: Boolean) { - binding.accountProgress.visibility = if (show) View.VISIBLE else View.GONE - } - - override fun showContent(show: Boolean) { - with(binding) { - accountRecycler.visibility = if (show) View.VISIBLE else View.GONE - accountAdd.visibility = if (show) View.VISIBLE else View.GONE - } - } } 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 366793b6d..8d1651395 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 @@ -5,7 +5,6 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -16,28 +15,13 @@ class AccountPresenter @Inject constructor( studentRepository: StudentRepository, ) : BasePresenter(errorHandler, studentRepository) { - private lateinit var lastError: Throwable - override fun onAttachView(view: AccountView) { super.onAttachView(view) view.initView() Timber.i("Account view was initialized") - errorHandler.showErrorMessage = ::showErrorViewOnError loadData() } - fun onRetry() { - view?.run { - showErrorView(false) - showProgress(true) - } - loadData() - } - - fun onDetailsClick() { - view?.showErrorDetailsDialog(lastError) - } - fun onAddSelected() { Timber.i("Select add account") view?.openLoginView() @@ -47,6 +31,24 @@ class AccountPresenter @Inject constructor( view?.openAccountDetailsView(studentWithSemesters) } + private fun loadData() { + flowWithResource { studentRepository.getSavedStudents(false) } + .onEach { + when (it.status) { + Status.LOADING -> Timber.i("Loading account data started") + Status.SUCCESS -> { + Timber.i("Loading account result: Success") + view?.updateData(createAccountItems(it.data!!)) + } + Status.ERROR -> { + Timber.i("Loading account result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + } + .launch("load") + } + private fun createAccountItems(items: List): List> { return items.groupBy { Account("${it.student.userName} (${it.student.email})", it.student.isParent) @@ -60,45 +62,4 @@ class AccountPresenter @Inject constructor( } .flatten() } - - private fun loadData() { - flowWithResource { studentRepository.getSavedStudents(false) } - .onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading account data started") - view?.run { - showProgress(true) - showContent(false) - } - } - Status.SUCCESS -> { - Timber.i("Loading account result: Success") - view?.updateData(createAccountItems(it.data!!)) - view?.run { - showContent(true) - showErrorView(false) - } - } - Status.ERROR -> { - Timber.i("Loading account result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - } - .afterLoading { view?.showProgress(false) } - .launch() - } - - private fun showErrorViewOnError(message: String, error: Throwable) { - view?.run { - if (isViewEmpty) { - lastError = error - setErrorDetails(message) - showErrorView(true) - showContent(false) - showProgress(false) - } else showError(message, error) - } - } } 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 3453909d1..d7deefafd 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 @@ -5,8 +5,6 @@ import io.github.wulkanowy.ui.base.BaseView interface AccountView : BaseView { - val isViewEmpty: Boolean - fun initView() fun updateData(data: List>) @@ -14,13 +12,4 @@ interface AccountView : BaseView { fun openLoginView() fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) - - fun showErrorView(show: Boolean) - - fun setErrorDetails(message: String) - - fun showProgress(show: Boolean) - - fun showContent(show: Boolean) } - diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt index f4b2c833d..f1c7f7bd1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import android.view.View import androidx.appcompat.app.AlertDialog import androidx.core.view.get +import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student @@ -18,6 +19,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView +import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.nickOrName import javax.inject.Inject @@ -31,8 +33,6 @@ class AccountDetailsFragment : override val titleStringId = R.string.account_details_title - override var subtitleString = "" - companion object { private const val ARGUMENT_KEY = "Data" @@ -88,8 +88,15 @@ class AccountDetailsFragment : override fun showAccountData(student: Student) { with(binding) { + accountDetailsCheck.isVisible = student.isCurrent accountDetailsName.text = student.nickOrName accountDetailsSchool.text = student.schoolName + accountDetailsAvatar.setImageDrawable( + requireContext().createNameInitialsDrawable( + student.nickOrName, + student.avatarColor + ) + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt index 7b93d3d87..cc53c9b63 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt @@ -21,7 +21,7 @@ class AccountDetailsPresenter @Inject constructor( private val syncManager: SyncManager ) : BasePresenter(errorHandler, studentRepository) { - private lateinit var studentWithSemesters: StudentWithSemesters + private var studentWithSemesters: StudentWithSemesters? = null private lateinit var lastError: Throwable @@ -69,10 +69,10 @@ class AccountDetailsPresenter @Inject constructor( } Status.SUCCESS -> { Timber.i("Loading account details view result: Success") - studentWithSemesters = it.data!! + studentWithSemesters = it.data view?.run { - showAccountData(studentWithSemesters.student) - enableSelectStudentButton(!studentWithSemesters.student.isCurrent) + showAccountData(studentWithSemesters!!.student) + enableSelectStudentButton(!studentWithSemesters!!.student.isCurrent) showContent(true) showErrorView(false) } @@ -88,17 +88,23 @@ class AccountDetailsPresenter @Inject constructor( } fun onAccountEditSelected() { - view?.showAccountEditDetailsDialog(studentWithSemesters.student) + studentWithSemesters?.let { + view?.showAccountEditDetailsDialog(it.student) + } } fun onStudentInfoSelected(infoType: StudentInfoView.Type) { - view?.openStudentInfoView(infoType, studentWithSemesters) + studentWithSemesters?.let { + view?.openStudentInfoView(infoType, it) + } } fun onStudentSelect() { - Timber.i("Select student ${studentWithSemesters.student.id}") + if (studentWithSemesters == null) return - flowWithResource { studentRepository.switchStudent(studentWithSemesters) } + Timber.i("Select student ${studentWithSemesters!!.student.id}") + + flowWithResource { studentRepository.switchStudent(studentWithSemesters!!) } .onEach { when (it.status) { Status.LOADING -> Timber.i("Attempt to change a student") @@ -122,8 +128,10 @@ class AccountDetailsPresenter @Inject constructor( } fun onLogoutConfirm() { + if (studentWithSemesters == null) return + flowWithResource { - val studentToLogout = studentWithSemesters.student + val studentToLogout = studentWithSemesters!!.student studentRepository.logoutStudent(studentToLogout) val students = studentRepository.getSavedStudents(false) @@ -143,7 +151,7 @@ class AccountDetailsPresenter @Inject constructor( syncManager.stopSyncWorker() openClearLoginView() } - studentWithSemesters.student.isCurrent -> { + studentWithSemesters!!.student.isCurrent -> { Timber.i("Logout result: Logout student and switch to another") recreateMainView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt new file mode 100644 index 000000000..ab6eec417 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt @@ -0,0 +1,90 @@ +package io.github.wulkanowy.ui.modules.account.accountedit + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable +import android.graphics.drawable.StateListDrawable +import android.os.Build +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.databinding.ItemAccountEditColorBinding +import javax.inject.Inject + +class AccountEditColorAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = listOf() + + var selectedColor = 0 + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAccountEditColorBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + @SuppressLint("RestrictedApi", "NewApi") + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + + with(holder.binding) { + accountEditItemColor.setImageDrawable(GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(item) + }) + + accountEditItemColorContainer.foreground = item.createForegroundDrawable() + accountEditCheck.isVisible = selectedColor == item + + root.setOnClickListener { + val oldSelectedPosition = items.indexOf(selectedColor) + selectedColor = item + + notifyItemChanged(oldSelectedPosition) + notifyItemChanged(position) + } + } + } + + private fun Int.createForegroundDrawable(): Drawable = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val mask = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(Color.BLACK) + } + RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) + } else { + val foreground = StateListDrawable().apply { + alpha = 80 + setEnterFadeDuration(250) + setExitFadeDuration(250) + } + + val mask = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(this@createForegroundDrawable.rippleColor) + } + + foreground.apply { + addState(intArrayOf(android.R.attr.state_pressed), mask) + addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT)) + } + } + + private inline val Int.rippleColor: Int + get() { + val hsv = FloatArray(3) + Color.colorToHSV(this, hsv) + hsv[2] = hsv[2] * 0.5f + return Color.HSVToColor(hsv) + } + + class ViewHolder(val binding: ItemAccountEditColorBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt index 89f23e29f..21a7a492d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.GridLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.databinding.DialogAccountEditBinding @@ -16,6 +17,9 @@ class AccountEditDialog : BaseDialogFragment(), Accoun @Inject lateinit var presenter: AccountEditPresenter + @Inject + lateinit var accountEditColorAdapter: AccountEditColorAdapter + companion object { private const val ARGUMENT_KEY = "student_with_semesters" @@ -48,8 +52,30 @@ class AccountEditDialog : BaseDialogFragment(), Accoun with(binding) { accountEditDetailsCancel.setOnClickListener { dismiss() } accountEditDetailsSave.setOnClickListener { - presenter.changeStudentNick(binding.accountEditDetailsNickText.text.toString()) + presenter.changeStudentNickAndAvatar( + binding.accountEditDetailsNickText.text.toString(), + accountEditColorAdapter.selectedColor + ) } + + with(binding.accountEditColors) { + layoutManager = GridLayoutManager(context, 4) + adapter = accountEditColorAdapter + } + } + } + + override fun updateSelectedColorData(color: Int) { + with(accountEditColorAdapter) { + selectedColor = color + notifyDataSetChanged() + } + } + + override fun updateColorsData(colors: List) { + with(accountEditColorAdapter) { + items = colors + notifyDataSetChanged() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt index 7830605c6..62dd70ab4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt @@ -2,10 +2,11 @@ package io.github.wulkanowy.ui.modules.account.accountedit import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.StudentNick +import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach @@ -13,12 +14,15 @@ import timber.log.Timber import javax.inject.Inject class AccountEditPresenter @Inject constructor( + private val appInfo: AppInfo, errorHandler: ErrorHandler, studentRepository: StudentRepository ) : BasePresenter(errorHandler, studentRepository) { lateinit var student: Student + private val colors = appInfo.defaultColorsForAvatar.map { it.toInt() } + fun onAttachView(view: AccountEditView, student: Student) { super.onAttachView(view) this.student = student @@ -28,27 +32,49 @@ class AccountEditPresenter @Inject constructor( showCurrentNick(student.nick.trim()) } Timber.i("Account edit dialog view was initialized") + loadData() + + view.updateColorsData(colors) } - fun changeStudentNick(nick: String) { + private fun loadData() { + flowWithResource { + studentRepository.getStudentById(student.id, false).avatarColor + }.onEach { resource -> + when (resource.status) { + Status.LOADING -> Timber.i("Attempt to load student") + Status.SUCCESS -> { + view?.updateSelectedColorData(resource.data?.toInt()!!) + Timber.i("Attempt to load student: Success") + } + Status.ERROR -> { + Timber.i("Attempt to load student: An exception occurred") + errorHandler.dispatch(resource.error!!) + } + } + }.launch("load_data") + } + + fun changeStudentNickAndAvatar(nick: String, avatarColor: Int) { flowWithResource { val studentNick = - StudentNick(nick = nick.trim()).apply { id = student.id } - studentRepository.updateStudentNick(studentNick) + StudentNickAndAvatar(nick = nick.trim(), avatarColor = avatarColor.toLong()) + .apply { id = student.id } + studentRepository.updateStudentNickAndAvatar(studentNick) }.onEach { when (it.status) { - Status.LOADING -> Timber.i("Attempt to change a student nick") + Status.LOADING -> Timber.i("Attempt to change a student nick and avatar") Status.SUCCESS -> { - Timber.i("Change a student nick result: Success") + Timber.i("Change a student nick and avatar result: Success") view?.recreateMainView() } Status.ERROR -> { - Timber.i("Change a student result: An exception occurred") + Timber.i("Change a student nick and avatar result: An exception occurred") errorHandler.dispatch(it.error!!) } } } .afterLoading { view?.popView() } - .launch() + .launch("update_student") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditView.kt index b25eec6c8..517492de1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditView.kt @@ -11,4 +11,8 @@ interface AccountEditView : BaseView { fun recreateMainView() fun showCurrentNick(nick: String) + + fun updateSelectedColorData(color: Int) + + fun updateColorsData(colors: List) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt index cb64a8fd3..4279102e1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.DialogAccountQuickBinding import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.modules.account.AccountAdapter @@ -24,7 +25,15 @@ class AccountQuickDialog : BaseDialogFragment(), Acco lateinit var presenter: AccountQuickPresenter companion object { - fun newInstance() = AccountQuickDialog() + + private const val STUDENTS_ARGUMENT_KEY = "students" + + fun newInstance(studentsWithSemesters: List) = + AccountQuickDialog().apply { + arguments = Bundle().apply { + putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray()) + } + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -38,8 +47,12 @@ class AccountQuickDialog : BaseDialogFragment(), Acco savedInstanceState: Bundle? ) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root + @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - presenter.onAttachView(this) + val studentsWithSemesters = + (requireArguments()[STUDENTS_ARGUMENT_KEY] as Array).toList() + + presenter.onAttachView(this, studentsWithSemesters) } override fun initView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt index 43cc8bc77..39d8fce24 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt @@ -17,11 +17,15 @@ class AccountQuickPresenter @Inject constructor( studentRepository: StudentRepository ) : BasePresenter(errorHandler, studentRepository) { - override fun onAttachView(view: AccountQuickView) { + private lateinit var studentsWithSemesters: List + + fun onAttachView(view: AccountQuickView, studentsWithSemesters: List) { super.onAttachView(view) + this.studentsWithSemesters = studentsWithSemesters + view.initView() Timber.i("Account quick dialog view was initialized") - loadData() + view.updateData(createAccountItems(studentsWithSemesters)) } fun onManagerSelected() { @@ -57,22 +61,6 @@ class AccountQuickPresenter @Inject constructor( .launch("switch") } - private fun loadData() { - flowWithResource { studentRepository.getSavedStudents(false) }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading account data started") - Status.SUCCESS -> { - Timber.i("Loading account result: Success") - view?.updateData(createAccountItems(it.data!!)) - } - Status.ERROR -> { - Timber.i("Loading account result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - }.launch() - } - private fun createAccountItems(items: List) = items.map { AccountItem(it, AccountItem.ViewType.ITEM) } 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 d5e2fe122..8b6526cdc 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 @@ -18,12 +18,11 @@ class AttendanceDialog : DialogFragment() { private lateinit var attendance: Attendance companion object { + private const val ARGUMENT_KEY = "Item" - fun newInstance(exam: Attendance): AttendanceDialog { - return AttendanceDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } - } + fun newInstance(exam: Attendance) = AttendanceDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } } } @@ -35,12 +34,14 @@ class AttendanceDialog : DialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) with(binding) { attendanceDialogSubject.text = attendance.subject 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 00d5aae87..cb718c724 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 @@ -192,7 +192,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showContent(show: Boolean) { - binding. attendanceRecycler.visibility = if (show) VISIBLE else GONE + binding.attendanceRecycler.visibility = if (show) VISIBLE else GONE } override fun showRefresh(show: Boolean) { 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 68802c0c7..b03db91af 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 @@ -190,35 +190,48 @@ class AttendancePresenter @Inject constructor( flowWithResourceIn { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) - attendanceRepository.getAttendance(student, semester, currentDate, currentDate, forceRefresh) + attendanceRepository.getAttendance( + student, + semester, + currentDate, + currentDate, + forceRefresh + ) }.onEach { when (it.status) { Status.LOADING -> { view?.showExcuseButton(false) if (!it.data.isNullOrEmpty()) { + val filteredAttendance = if (prefRepository.isShowPresent) { + it.data + } else { + it.data.filter { item -> !item.presence } + } + view?.run { enableSwipe(true) showRefresh(true) showProgress(false) - showContent(true) - updateData(it.data.let { items -> - if (prefRepository.isShowPresent) items - else items.filter { item -> !item.presence } - }.sortedBy { item -> item.number }) + showEmpty(filteredAttendance.isEmpty()) + showContent(filteredAttendance.isNotEmpty()) + updateData(filteredAttendance.sortedBy { item -> item.number }) } } } Status.SUCCESS -> { Timber.i("Loading attendance result: Success") + val filteredAttendance = if (prefRepository.isShowPresent) { + it.data.orEmpty() + } else { + it.data?.filter { item -> !item.presence }.orEmpty() + } + view?.apply { - updateData(it.data!!.let { items -> - if (prefRepository.isShowPresent) items - else items.filter { item -> !item.presence } - }.sortedBy { item -> item.number }) - showEmpty(it.data.isEmpty()) + updateData(filteredAttendance.sortedBy { item -> item.number }) + showEmpty(filteredAttendance.isEmpty()) showErrorView(false) - showContent(it.data.isNotEmpty()) - showExcuseButton(it.data.any { item -> item.excusable }) + showContent(filteredAttendance.isNotEmpty()) + showExcuseButton(filteredAttendance.any { item -> item.excusable }) } analytics.logEvent( "load_data", 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..3f815a2cb 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 @@ -17,12 +17,11 @@ class ExamDialog : DialogFragment() { private lateinit var exam: Exam companion object { + private const val ARGUMENT_KEY = "Item" - fun newInstance(exam: Exam): ExamDialog { - return ExamDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } - } + fun newInstance(exam: Exam) = ExamDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } } } @@ -34,12 +33,14 @@ class ExamDialog : DialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogExamBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogExamBinding.inflate(inflater).apply { binding = this }.root - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) with(binding) { examDialogSubjectValue.text = exam.subject 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 91e39e068..b3ef3037a 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 @@ -10,6 +10,7 @@ import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.databinding.FragmentGradeBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter @@ -63,11 +64,13 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade override fun initView() { with(pagerAdapter) { containerId = binding.gradeViewPager.id - addFragmentsWithTitle(mapOf( - GradeDetailsFragment.newInstance() to getString(R.string.all_details), - GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), - GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics) - )) + addFragmentsWithTitle( + mapOf( + GradeDetailsFragment.newInstance() to getString(R.string.all_details), + GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), + GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics) + ) + ) } with(binding.gradeViewPager) { @@ -119,11 +122,9 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade semesterSwitchMenu?.isVisible = show } - override fun showSemesterDialog(selectedIndex: Int) { - val choices = arrayOf( - getString(R.string.grade_semester, 1), - getString(R.string.grade_semester, 2) - ) + override fun showSemesterDialog(selectedIndex: Int, semesters: List) { + val choices = semesters.map { getString(R.string.grade_semester, it.semesterName) } + .toTypedArray() AlertDialog.Builder(requireContext()) .setSingleChoiceItems(choices, selectedIndex) { dialog, which -> @@ -137,7 +138,10 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade override fun setCurrentSemesterName(semester: Int, schoolYear: Int) { subtitleString = getString(R.string.grade_subtitle, semester, schoolYear, schoolYear + 1) - (activity as MainView).setViewSubTitle(subtitleString) + + if (isVisible) { + (activity as MainView?)?.setViewSubTitle(subtitleString) + } } fun onChildRefresh() { @@ -149,7 +153,8 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade } override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) { - (pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentLoadData(semesterId, forceRefresh) + (pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView) + ?.onParentLoadData(semesterId, forceRefresh) } override fun notifyChildParentReselected(index: Int) { 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 bfc504d2c..c467c421e 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 @@ -49,7 +49,9 @@ class GradePresenter @Inject constructor( } fun onSemesterSwitch(): Boolean { - if (semesters.isNotEmpty()) view?.showSemesterDialog(selectedIndex - 1) + if (semesters.isNotEmpty()) { + view?.showSemesterDialog(selectedIndex - 1, semesters.slice(0..1)) + } return true } @@ -137,11 +139,17 @@ class GradePresenter @Inject constructor( private fun loadChild(index: Int, forceRefresh: Boolean = false) { Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}") - semesters.first { it.semesterName == selectedIndex }.semesterId.also { - if (forceRefresh || loadedSemesterId[index] != it) { - Timber.i("Load grade child view index: $index") - view?.notifyChildLoadData(index, it, forceRefresh) - } + + val newSelectedSemesterId = try { + semesters.first { it.semesterName == selectedIndex }.semesterId + } catch (e: NoSuchElementException) { + Timber.e(e, "Selected semester no exists") + return + } + + if (forceRefresh || loadedSemesterId[index] != newSelectedSemesterId) { + Timber.i("Load grade child view index: $index") + view?.notifyChildLoadData(index, newSelectedSemesterId, forceRefresh) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeView.kt index 7b52daa88..104f8505a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.grade +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.ui.base.BaseView interface GradeView : BaseView { @@ -18,7 +19,7 @@ interface GradeView : BaseView { fun showSemesterSwitch(show: Boolean) - fun showSemesterDialog(selectedIndex: Int) + fun showSemesterDialog(selectedIndex: Int, semesters: List) fun setCurrentSemesterName(semester: Int, schoolYear: Int) 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..286194464 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 @@ -24,17 +24,18 @@ class GradeDetailsDialog : DialogFragment() { private lateinit var colorScheme: String companion object { + private const val ARGUMENT_KEY = "Item" + private const val COLOR_SCHEME_KEY = "Scheme" - fun newInstance(grade: Grade, colorScheme: String): GradeDetailsDialog { - return GradeDetailsDialog().apply { + fun newInstance(grade: Grade, colorScheme: String) = + GradeDetailsDialog().apply { arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, grade) putString(COLOR_SCHEME_KEY, colorScheme) } } - } } override fun onCreate(savedInstanceState: Bundle?) { @@ -46,12 +47,14 @@ class GradeDetailsDialog : DialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogGradeBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogGradeBinding.inflate(inflater).apply { binding = this }.root - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) with(binding) { gradeDialogSubject.text = grade.subject 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 0ffb42257..bf0b20142 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 @@ -22,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics import io.github.wulkanowy.data.pojos.GradeStatisticsItem import io.github.wulkanowy.databinding.ItemGradeStatisticsBarBinding +import io.github.wulkanowy.databinding.ItemGradeStatisticsHeaderBinding import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding import io.github.wulkanowy.utils.getThemeAttrColor import javax.inject.Inject @@ -29,12 +30,16 @@ import javax.inject.Inject class GradeStatisticsAdapter @Inject constructor() : RecyclerView.Adapter() { + var currentDataType = GradeStatisticsItem.DataType.PARTIAL + var items = emptyList() var theme: String = "vulcan" var showAllSubjectsOnList: Boolean = false + var onDataTypeChangeListener: () -> Unit = {} + private val vulcanGradeColors = listOf( 6 to R.color.grade_vulcan_six, 5 to R.color.grade_vulcan_five, @@ -62,37 +67,90 @@ 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() = + (if (showAllSubjectsOnList) items.size else (if (items.isEmpty()) 0 else 1)) + 1 - override fun getItemViewType(position: Int) = items[position].type.id + override fun getItemViewType(position: Int) = + if (position == 0) { + ViewType.HEADER.id + } else { + when (items[position - 1].type) { + GradeStatisticsItem.DataType.PARTIAL -> ViewType.PARTIAL.id + GradeStatisticsItem.DataType.POINTS -> ViewType.POINTS.id + GradeStatisticsItem.DataType.SEMESTER -> ViewType.SEMESTER.id + } + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.PARTIAL.id -> PartialViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)) - ViewType.SEMESTER.id -> SemesterViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)) - ViewType.POINTS.id -> PointsViewHolder(ItemGradeStatisticsBarBinding.inflate(inflater, parent, false)) + ViewType.PARTIAL.id -> PartialViewHolder( + ItemGradeStatisticsPieBinding.inflate(inflater, parent, false) + ) + ViewType.SEMESTER.id -> SemesterViewHolder( + ItemGradeStatisticsPieBinding.inflate(inflater, parent, false) + ) + ViewType.POINTS.id -> PointsViewHolder( + ItemGradeStatisticsBarBinding.inflate(inflater, parent, false) + ) + ViewType.HEADER.id -> HeaderViewHolder( + ItemGradeStatisticsHeaderBinding.inflate(inflater, parent, false) + ) else -> throw IllegalStateException() } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val index = position - 1 + when (holder) { - is PartialViewHolder -> bindPartialChart(holder.binding, items[position].partial!!) - is SemesterViewHolder -> bindSemesterChart(holder.binding, items[position].semester!!) - is PointsViewHolder -> bindBarChart(holder.binding, items[position].points!!) + is PartialViewHolder -> bindPartialChart(holder.binding, items[index].partial!!) + is SemesterViewHolder -> bindSemesterChart(holder.binding, items[index].semester!!) + is PointsViewHolder -> bindBarChart(holder.binding, items[index].points!!) + is HeaderViewHolder -> bindHeader(holder.binding) } } - private fun bindPartialChart(binding: ItemGradeStatisticsPieBinding, partials: GradePartialStatistics) { + private fun bindHeader(binding: ItemGradeStatisticsHeaderBinding) { + binding.gradeStatisticsTypeSwitch.check( + when (currentDataType) { + GradeStatisticsItem.DataType.PARTIAL -> R.id.gradeStatisticsTypePartial + GradeStatisticsItem.DataType.SEMESTER -> R.id.gradeStatisticsTypeSemester + GradeStatisticsItem.DataType.POINTS -> R.id.gradeStatisticsTypePoints + } + ) + + binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, checkedId -> + currentDataType = when (checkedId) { + R.id.gradeStatisticsTypePartial -> GradeStatisticsItem.DataType.PARTIAL + R.id.gradeStatisticsTypeSemester -> GradeStatisticsItem.DataType.SEMESTER + R.id.gradeStatisticsTypePoints -> GradeStatisticsItem.DataType.POINTS + else -> GradeStatisticsItem.DataType.PARTIAL + } + onDataTypeChangeListener() + } + } + + private fun bindPartialChart( + binding: ItemGradeStatisticsPieBinding, + partials: GradePartialStatistics + ) { bindPieChart(binding, partials.subject, partials.classAverage, partials.classAmounts) } - private fun bindSemesterChart(binding: ItemGradeStatisticsPieBinding, semester: GradeSemesterStatistics) { + private fun bindSemesterChart( + binding: ItemGradeStatisticsPieBinding, + semester: GradeSemesterStatistics + ) { bindPieChart(binding, semester.subject, semester.average, semester.amounts) } - private fun bindPieChart(binding: ItemGradeStatisticsPieBinding, subject: String, average: String, amounts: List) { + private fun bindPieChart( + binding: ItemGradeStatisticsPieBinding, + subject: String, + average: String, + amounts: List + ) { with(binding.gradeStatisticsPieTitle) { text = subject visibility = if (items.size == 1 || !showAllSubjectsOnList) GONE else VISIBLE @@ -114,7 +172,8 @@ class GradeStatisticsAdapter @Inject constructor() : valueTextSize = 12f sliceSpace = 1f valueTextColor = Color.WHITE - val grades = amounts.mapIndexed { grade, amount -> (grade + 1) to amount }.filterNot { it.second == 0 } + val grades = amounts.mapIndexed { grade, amount -> (grade + 1) to amount } + .filterNot { it.second == 0 } setColors(grades.reversed().map { (grade, _) -> gradeColors.single { color -> color.first == grade }.second }.toIntArray(), binding.root.context) @@ -126,7 +185,11 @@ class GradeStatisticsAdapter @Inject constructor() : data = PieData(dataset).apply { setValueFormatter(object : ValueFormatter() { override fun getPieLabel(value: Float, pieEntry: PieEntry): String { - return resources.getQuantityString(R.plurals.grade_number_item, value.toInt(), value.toInt()) + return resources.getQuantityString( + R.plurals.grade_number_item, + value.toInt(), + value.toInt() + ) } }) } @@ -143,11 +206,14 @@ class GradeStatisticsAdapter @Inject constructor() : val numberOfGradesString = amounts.fold(0) { acc, it -> acc + it } .let { resources.getQuantityString(R.plurals.grade_number_item, it, it) } - val averageString = binding.root.context.getString(R.string.grade_statistics_average, average) + val averageString = + binding.root.context.getString(R.string.grade_statistics_average, average) minAngleForSlices = 25f description.isEnabled = false - centerText = numberOfGradesString + ("\n\n" + averageString).takeIf { average.isNotBlank() }.orEmpty() + centerText = + numberOfGradesString + ("\n\n" + averageString).takeIf { average.isNotBlank() } + .orEmpty() setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground)) setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary)) @@ -155,16 +221,21 @@ class GradeStatisticsAdapter @Inject constructor() : } } - private fun bindBarChart(binding: ItemGradeStatisticsBarBinding, points: GradePointsStatistics) { + private fun bindBarChart( + binding: ItemGradeStatisticsBarBinding, + points: GradePointsStatistics + ) { with(binding.gradeStatisticsBarTitle) { text = points.subject visibility = if (items.size == 1) GONE else VISIBLE } - val dataset = BarDataSet(listOf( - BarEntry(1f, points.others.toFloat()), - BarEntry(2f, points.student.toFloat()) - ), binding.root.context.getString(R.string.grade_statistics_legend)) + val dataset = BarDataSet( + listOf( + BarEntry(1f, points.others.toFloat()), + BarEntry(2f, points.student.toFloat()) + ), binding.root.context.getString(R.string.grade_statistics_legend) + ) with(dataset) { valueTextSize = 12f @@ -189,7 +260,8 @@ class GradeStatisticsAdapter @Inject constructor() : form = Legend.LegendForm.SQUARE }, LegendEntry().apply { - label = binding.root.context.getString(R.string.grade_statistics_average_student) + label = + binding.root.context.getString(R.string.grade_statistics_average_student) formColor = gradePointsColors[1] form = Legend.LegendForm.SQUARE } @@ -226,4 +298,7 @@ class GradeStatisticsAdapter @Inject constructor() : private class PointsViewHolder(val binding: ItemGradeStatisticsBarBinding) : RecyclerView.ViewHolder(binding.root) + + private class HeaderViewHolder(val binding: ItemGradeStatisticsHeaderBinding) : + RecyclerView.ViewHolder(binding.root) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt index 1ce7a2020..0adac300a 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 @@ -38,27 +38,28 @@ class GradeStatisticsFragment : override val isViewEmpty get() = statisticsAdapter.items.isEmpty() - override val currentType - get() = when (binding.gradeStatisticsTypeSwitch.checkedRadioButtonId) { - R.id.gradeStatisticsTypeSemester -> ViewType.SEMESTER - R.id.gradeStatisticsTypePartial -> ViewType.PARTIAL - else -> ViewType.POINTS - } + override val currentType get() = statisticsAdapter.currentDataType override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentGradeStatisticsBinding.bind(view) messageContainer = binding.gradeStatisticsSwipe - presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? ViewType) + presenter.onAttachView( + this, + savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType + ) } override fun initView() { + statisticsAdapter.onDataTypeChangeListener = presenter::onTypeChange + with(binding.gradeStatisticsRecycler) { layoutManager = LinearLayoutManager(requireContext()) adapter = statisticsAdapter } - subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) + subjectsAdapter = + ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject) with(binding.gradeStatisticsSubjects) { @@ -71,7 +72,9 @@ class GradeStatisticsFragment : gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - gradeStatisticsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + gradeStatisticsSwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) + ) gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() } gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() } } @@ -85,11 +88,15 @@ class GradeStatisticsFragment : } } - override fun updateData(items: List, theme: String, showAllSubjectsOnStatisticsList: Boolean) { + override fun updateData( + newItems: List, + newTheme: String, + showAllSubjectsOnStatisticsList: Boolean + ) { with(statisticsAdapter) { - this.showAllSubjectsOnList = showAllSubjectsOnStatisticsList - this.theme = theme - this.items = items + showAllSubjectsOnList = showAllSubjectsOnStatisticsList + theme = newTheme + items = newItems notifyDataSetChanged() } } @@ -103,11 +110,7 @@ class GradeStatisticsFragment : } override fun resetView() { - binding.gradeStatisticsScroll.scrollTo(0, 0) - } - - override fun showContent(show: Boolean) { - binding.gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE + binding.gradeStatisticsRecycler.scrollToPosition(0) } override fun showEmpty(show: Boolean) { @@ -154,11 +157,6 @@ class GradeStatisticsFragment : (parentFragment as? GradeFragment)?.onChildRefresh() } - override fun onResume() { - super.onResume() - binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() } - } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putSerializable(SAVED_CHART_TYPE, presenter.currentType) 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 47ea52d35..53eccad65 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 @@ -35,12 +35,12 @@ class GradeStatisticsPresenter @Inject constructor( private lateinit var lastError: Throwable - var currentType: ViewType = ViewType.PARTIAL + var currentType: GradeStatisticsItem.DataType = GradeStatisticsItem.DataType.PARTIAL private set - fun onAttachView(view: GradeStatisticsView, type: ViewType?) { + fun onAttachView(view: GradeStatisticsView, type: GradeStatisticsItem.DataType?) { super.onAttachView(view) - currentType = type ?: ViewType.PARTIAL + currentType = type ?: GradeStatisticsItem.DataType.PARTIAL view.initView() errorHandler.showErrorMessage = ::showErrorViewOnError } @@ -59,11 +59,11 @@ class GradeStatisticsPresenter @Inject constructor( } fun onParentViewChangeSemester() { + clearDataInView() view?.run { showProgress(true) enableSwipe(false) showRefresh(false) - showContent(false) showErrorView(false) showEmpty(false) clearView() @@ -90,8 +90,8 @@ class GradeStatisticsPresenter @Inject constructor( fun onSubjectSelected(name: String?) { Timber.i("Select grade stats subject $name") + clearDataInView() view?.run { - showContent(false) showProgress(true) enableSwipe(false) showEmpty(false) @@ -104,11 +104,11 @@ class GradeStatisticsPresenter @Inject constructor( } fun onTypeChange() { - val type = view?.currentType ?: ViewType.POINTS + val type = view?.currentType ?: GradeStatisticsItem.DataType.POINTS Timber.i("Select grade stats semester: $type") cancelJobs("load") + clearDataInView() view?.run { - showContent(false) showProgress(true) enableSwipe(false) showEmpty(false) @@ -143,10 +143,16 @@ class GradeStatisticsPresenter @Inject constructor( }.launch("subjects") } - private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) { + private fun loadDataByType( + semesterId: Int, + subjectName: String, + type: GradeStatisticsItem.DataType, + forceRefresh: Boolean = false + ) { Timber.i("Loading grade stats data started") - currentSubjectName = if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName + currentSubjectName = + if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName currentType = type flowWithResourceIn { @@ -156,9 +162,30 @@ class GradeStatisticsPresenter @Inject constructor( with(gradeStatisticsRepository) { when (type) { - ViewType.PARTIAL -> getGradesPartialStatistics(student, semester, currentSubjectName, forceRefresh) - ViewType.SEMESTER -> getGradesSemesterStatistics(student, semester, currentSubjectName, forceRefresh) - ViewType.POINTS -> getGradesPointsStatistics(student, semester, currentSubjectName, forceRefresh) + GradeStatisticsItem.DataType.PARTIAL -> { + getGradesPartialStatistics( + student = student, + semester = semester, + subjectName = currentSubjectName, + forceRefresh = forceRefresh + ) + } + GradeStatisticsItem.DataType.SEMESTER -> { + getGradesSemesterStatistics( + student = student, + semester = semester, + subjectName = currentSubjectName, + forceRefresh = forceRefresh + ) + } + GradeStatisticsItem.DataType.POINTS -> { + getGradesPointsStatistics( + student = student, + semester = semester, + subjectName = currentSubjectName, + forceRefresh = forceRefresh + ) + } } } }.onEach { @@ -168,12 +195,15 @@ class GradeStatisticsPresenter @Inject constructor( if (!isNoContent) { view?.run { showEmpty(isNoContent) - showContent(!isNoContent) showErrorView(false) enableSwipe(true) showRefresh(true) showProgress(false) - updateData(it.data!!, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList) + updateData( + if (isNoContent) emptyList() else it.data!!, + preferencesRepository.gradeColorTheme, + preferencesRepository.showAllSubjectsOnStatisticsList + ) showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) } } @@ -183,9 +213,12 @@ class GradeStatisticsPresenter @Inject constructor( view?.run { val isNoContent = checkIsNoContent(it.data!!, type) showEmpty(isNoContent) - showContent(!isNoContent) showErrorView(false) - updateData(it.data, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList) + updateData( + if (isNoContent) emptyList() else it.data, + preferencesRepository.gradeColorTheme, + preferencesRepository.showAllSubjectsOnStatisticsList + ) showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) } analytics.logEvent( @@ -209,16 +242,31 @@ class GradeStatisticsPresenter @Inject constructor( }.launch("load") } - private fun checkIsNoContent(items: List, type: ViewType): Boolean { + private fun checkIsNoContent( + items: List, + type: GradeStatisticsItem.DataType + ): Boolean { return items.isEmpty() || when (type) { - ViewType.SEMESTER -> items.firstOrNull()?.semester?.amounts.orEmpty().sum() == 0 - ViewType.PARTIAL -> items.firstOrNull()?.partial?.classAmounts.orEmpty().sum() == 0 - ViewType.POINTS -> items.firstOrNull()?.points?.let { points -> - points.student == .0 && points.others == .0 - } ?: false + GradeStatisticsItem.DataType.SEMESTER -> { + items.firstOrNull()?.semester?.amounts.orEmpty().sum() == 0 + } + GradeStatisticsItem.DataType.PARTIAL -> { + items.firstOrNull()?.partial?.classAmounts.orEmpty().sum() == 0 + } + GradeStatisticsItem.DataType.POINTS -> { + items.firstOrNull()?.points?.let { points -> points.student == .0 && points.others == .0 } ?: false + } } } + private fun clearDataInView() { + view?.updateData( + emptyList(), + preferencesRepository.gradeColorTheme, + preferencesRepository.showAllSubjectsOnStatisticsList + ) + } + private fun showErrorViewOnError(message: String, error: Throwable) { view?.run { if (isViewEmpty) { 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..405118178 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 @@ -7,13 +7,17 @@ interface GradeStatisticsView : BaseView { val isViewEmpty: Boolean - val currentType: ViewType + val currentType: GradeStatisticsItem.DataType fun initView() fun updateSubjects(data: ArrayList) - fun updateData(items: List, theme: String, showAllSubjectsOnStatisticsList: Boolean) + fun updateData( + newItems: List, + newTheme: String, + showAllSubjectsOnStatisticsList: Boolean + ) fun showSubjects(show: Boolean) @@ -25,8 +29,6 @@ interface GradeStatisticsView : BaseView { fun resetView() - fun showContent(show: Boolean) - fun showEmpty(show: Boolean) fun showErrorView(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..f695eaf9d 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 @@ -3,5 +3,6 @@ package io.github.wulkanowy.ui.modules.grade.statistics enum class ViewType(val id: Int) { SEMESTER(1), PARTIAL(2), - POINTS(3) + POINTS(3), + HEADER(4) } 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 aecaa394d..93045a481 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 @@ -28,12 +28,11 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew private lateinit var homework: Homework companion object { + private const val ARGUMENT_KEY = "Item" - fun newInstance(homework: Homework): HomeworkDetailsDialog { - return HomeworkDetailsDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) } - } + fun newInstance(homework: Homework) = HomeworkDetailsDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) } } } @@ -45,19 +44,22 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) presenter.onAttachView(this) } @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.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() } } @@ -87,7 +89,8 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew } override fun updateMarkAsDoneLabel(isDone: Boolean) { - binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) + binding.homeworkDialogRead.text = + view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index aff1c84ca..8d96a498f 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 @@ -52,6 +52,8 @@ class LoginActivity : BaseActivity(), Logi updateHelper.onResume(this) } + //https://developer.android.com/guide/playcore/in-app-updates#status_callback + @Suppress("DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) updateHelper.onActivityResult(requestCode, resultCode) @@ -65,13 +67,15 @@ class LoginActivity : BaseActivity(), Logi with(loginAdapter) { containerId = binding.loginViewpager.id - addFragments(listOf( - LoginFormFragment.newInstance(), - LoginSymbolFragment.newInstance(), - LoginStudentSelectFragment.newInstance(), - LoginAdvancedFragment.newInstance(), - LoginRecoverFragment.newInstance() - )) + addFragments( + listOf( + LoginFormFragment.newInstance(), + LoginSymbolFragment.newInstance(), + LoginStudentSelectFragment.newInstance(), + LoginAdvancedFragment.newInstance(), + LoginRecoverFragment.newInstance() + ) + ) } with(binding.loginViewpager) { @@ -99,14 +103,20 @@ class LoginActivity : BaseActivity(), Logi } override fun notifyInitSymbolFragment(loginData: Triple) { - (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment(loginData) + (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment( + loginData + ) } override fun notifyInitStudentSelectFragment(studentsWithSemesters: List) { - (loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment)?.onParentInitStudentSelectFragment(studentsWithSemesters) + (loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) + ?.onParentInitStudentSelectFragment(studentsWithSemesters) } - fun onFormFragmentAccountLogged(studentsWithSemesters: List, loginData: Triple) { + fun onFormFragmentAccountLogged( + studentsWithSemesters: List, + loginData: Triple + ) { presenter.onFormViewAccountLogged(studentsWithSemesters, loginData) } 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 c6d0209da..4e09bd4d6 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 @@ -150,6 +150,13 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } } + override fun setErrorEmailInvalid(domain: String) { + with(binding.loginFormUsernameLayout) { + requestFocus() + error = getString(R.string.login_invalid_custom_email,domain) + } + } + override fun clearUsernameError() { binding.loginFormUsernameLayout.error = null } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index f6a528ae3..70b468990 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.ifNullOrBlank import kotlinx.coroutines.flow.onEach import timber.log.Timber +import java.net.URL import javax.inject.Inject class LoginFormPresenter @Inject constructor( @@ -87,7 +88,14 @@ class LoginFormPresenter @Inject constructor( if (!validateCredentials(email, password, host)) return - flowWithResource { studentRepository.getStudentsScrapper(email, password, host, symbol) }.onEach { + flowWithResource { + studentRepository.getStudentsScrapper( + email, + password, + host, + symbol + ) + }.onEach { when (it.status) { Status.LOADING -> view?.run { Timber.i("Login started") @@ -150,11 +158,18 @@ class LoginFormPresenter @Inject constructor( view?.setErrorLoginRequired() isCorrect = false } - if ("@" !in login && "email" in host) { view?.setErrorEmailRequired() isCorrect = false } + if ("@" in login && "login" !in host && "email" !in host) { + val emailHost = login.substringAfter("@") + val emailDomain = URL(host).host + if (emailHost != emailDomain) { + view?.setErrorEmailInvalid(domain = emailDomain) + isCorrect = false + } + } } if (password.isEmpty()) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index 31f8a6217..079629ef6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -39,6 +39,8 @@ interface LoginFormView : BaseView { fun setErrorPassIncorrect() + fun setErrorEmailInvalid(domain: String) + fun clearUsernameError() fun clearPassError() 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 3de235857..0a73fe15d 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 @@ -9,6 +9,8 @@ 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.luckynumber.history.LuckyNumberHistoryFragment +import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.getThemeAttrColor import javax.inject.Inject @@ -42,6 +44,7 @@ class LuckyNumberFragment : luckyNumberSwipe.setOnRefreshListener(presenter::onSwipeRefresh) luckyNumberSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) luckyNumberSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + luckyNumberHistoryButton.setOnClickListener { openLuckyNumberHistory() } luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() } luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() } } @@ -79,6 +82,10 @@ class LuckyNumberFragment : binding.luckyNumberContent.visibility = if (show) VISIBLE else GONE } + override fun openLuckyNumberHistory() { + (activity as? MainActivity)?.pushView(LuckyNumberHistoryFragment.newInstance()) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt index a680c83eb..0c05a1566 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt @@ -24,4 +24,6 @@ interface LuckyNumberView : BaseView { fun enableSwipe(enable: Boolean) fun showContent(show: Boolean) + + fun openLuckyNumberHistory() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt new file mode 100644 index 000000000..7c09c96fe --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.ui.modules.luckynumber.history + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.data.db.entities.LuckyNumber +import io.github.wulkanowy.databinding.ItemLuckyNumberHistoryBinding +import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.weekDayName +import java.util.Locale +import javax.inject.Inject + +class LuckyNumberHistoryAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = emptyList() + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemLuckyNumberHistoryBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + @SuppressLint("DefaultLocale") + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val item = items[position] + with(holder.binding) { + luckyNumberHistoryWeekName.text = item.date.weekDayName.capitalize() + luckyNumberHistoryDate.text = item.date.toFormattedString() + luckyNumberHistory.text = item.luckyNumber.toString() + } + } + + class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt new file mode 100644 index 000000000..6e991ba13 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt @@ -0,0 +1,134 @@ +package io.github.wulkanowy.ui.modules.luckynumber.history + +import android.os.Bundle +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.recyclerview.widget.LinearLayoutManager +import com.wdullaer.materialdatetimepicker.date.DatePickerDialog +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.LuckyNumber +import io.github.wulkanowy.databinding.FragmentLuckyNumberHistoryBinding +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.SchooldaysRangeLimiter +import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.getThemeAttrColor +import java.time.LocalDate +import javax.inject.Inject + +@AndroidEntryPoint +class LuckyNumberHistoryFragment : + BaseFragment(R.layout.fragment_lucky_number_history), LuckyNumberHistoryView, + MainView.TitledView { + + @Inject + lateinit var presenter: LuckyNumberHistoryPresenter + + @Inject + lateinit var luckyNumberHistoryAdapter: LuckyNumberHistoryAdapter + + companion object { + fun newInstance() = LuckyNumberHistoryFragment() + } + + override val titleStringId: Int + get() = R.string.lucky_number_history_title + + override val isViewEmpty get() = luckyNumberHistoryAdapter.items.isEmpty() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentLuckyNumberHistoryBinding.bind(view) + messageContainer = binding.luckyNumberHistoryRecycler + presenter.onAttachView(this) + } + + override fun initView() { + with(binding.luckyNumberHistoryRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = luckyNumberHistoryAdapter + addItemDecoration(DividerItemDecoration(context)) + } + + with(binding) { + luckyNumberHistoryNavDate.setOnClickListener { presenter.onPickDate() } + luckyNumberHistoryErrorRetry.setOnClickListener { presenter.onRetry() } + luckyNumberHistoryErrorDetails.setOnClickListener { presenter.onDetailsClick() } + + luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() } + luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() } + + luckyNumberHistoryNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + } + } + + override fun updateData(data: List) { + with(luckyNumberHistoryAdapter) { + items = data + notifyDataSetChanged() + } + } + + override fun clearData() { + with(luckyNumberHistoryAdapter) { + items = emptyList() + notifyDataSetChanged() + } + } + + override fun showEmpty(show: Boolean) { + binding.luckyNumberHistoryEmpty.visibility = if (show) VISIBLE else GONE + } + + override fun showErrorView(show: Boolean) { + binding.luckyNumberHistoryError.visibility = if (show) VISIBLE else GONE + } + + override fun setErrorDetails(message: String) { + binding.luckyNumberHistoryErrorMessage.text = message + } + + override fun updateNavigationWeek(date: String) { + binding.luckyNumberHistoryNavDate.text = date + } + + override fun showProgress(show: Boolean) { + binding.luckyNumberHistoryProgress.visibility = if (show) VISIBLE else GONE + } + + override fun showPreButton(show: Boolean) { + binding.luckyNumberHistoryPreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE + } + + override fun showNextButton(show: Boolean) { + binding.luckyNumberHistoryNextButton.visibility = if (show) VISIBLE else View.INVISIBLE + } + + override fun showDatePickerDialog(currentDate: LocalDate) { + val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth -> + presenter.onDateSet(year, month + 1, dayOfMonth) + } + val datePickerDialog = DatePickerDialog.newInstance(dateSetListener, + currentDate.year, currentDate.monthValue - 1, currentDate.dayOfMonth) + + with(datePickerDialog) { + setDateRangeLimiter(SchooldaysRangeLimiter()) + version = DatePickerDialog.Version.VERSION_2 + scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL + vibrate(false) + show(this@LuckyNumberHistoryFragment.parentFragmentManager, null) + } + } + + override fun showContent(show: Boolean) { + binding.luckyNumberHistoryRecycler.visibility = if (show) VISIBLE else GONE + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt new file mode 100644 index 000000000..556dda759 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt @@ -0,0 +1,151 @@ +package io.github.wulkanowy.ui.modules.luckynumber.history + +import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.repositories.LuckyNumberRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.afterLoading +import io.github.wulkanowy.utils.flowWithResource +import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.previousOrSameSchoolDay +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.toFormattedString +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.time.LocalDate +import javax.inject.Inject + +class LuckyNumberHistoryPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val luckyNumberRepository: LuckyNumberRepository, + private val analytics: AnalyticsHelper +) : BasePresenter(errorHandler, studentRepository) { + + private lateinit var lastError: Throwable + + var currentDate: LocalDate = LocalDate.now().previousOrSameSchoolDay + + override fun onAttachView(view: LuckyNumberHistoryView) { + super.onAttachView(view) + view.run { + initView() + reloadNavigation() + showContent(false) + } + Timber.i("Lucky number history view was initialized") + errorHandler.showErrorMessage = ::showErrorViewOnError + loadData() + } + + private fun loadData() { + flowWithResource { + val student = studentRepository.getCurrentStudent() + luckyNumberRepository.getLuckyNumberHistory(student, currentDate.monday, currentDate.sunday) + }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Loading lucky number history started") + Status.SUCCESS -> { + if (!it.data?.first().isNullOrEmpty()) { + Timber.i("Loading lucky number result: Success") + view?.apply { + updateData(it.data!!.first()) + showContent(true) + showEmpty(false) + showErrorView(false) + showProgress(false) + } + analytics.logEvent( + "load_items", + "type" to "lucky_number_history", + "numbers" to it.data + ) + } else { + Timber.i("Loading lucky number history result: No lucky numbers found") + view?.run { + showContent(false) + showEmpty(true) + showErrorView(false) + } + } + } + Status.ERROR -> { + Timber.i("Loading lucky number history result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.afterLoading { + view?.run { + showProgress(false) + } + }.launch() + } + + private fun showErrorViewOnError(message: String, error: Throwable) { + view?.run { + if (isViewEmpty) { + lastError = error + setErrorDetails(message) + showErrorView(true) + showEmpty(false) + } else showError(message, error) + } + } + + private fun reloadView(date: LocalDate) { + currentDate = date + Timber.i("Reload lucky number history view with the date ${currentDate.toFormattedString()}") + view?.apply { + showProgress(true) + showContent(false) + showEmpty(false) + showErrorView(false) + clearData() + reloadNavigation() + } + } + + fun onRetry() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData() + } + + fun onDetailsClick() { + view?.showErrorDetailsDialog(lastError) + } + + private fun reloadNavigation() { + view?.apply { + showPreButton(!currentDate.minusDays(7).isHolidays) + showNextButton(!currentDate.plusDays(7).isHolidays) + updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " + + currentDate.sunday.toFormattedString("dd.MM")) + } + } + + fun onDateSet(year: Int, month: Int, day: Int) { + reloadView(LocalDate.of(year, month, day)) + loadData() + } + + fun onPickDate() { + view?.showDatePickerDialog(currentDate) + } + + fun onPreviousWeek() { + reloadView(currentDate.minusDays(7)) + loadData() + } + + fun onNextWeek() { + reloadView(currentDate.plusDays(7)) + loadData() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryView.kt new file mode 100644 index 000000000..331e4ff86 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryView.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.ui.modules.luckynumber.history + +import io.github.wulkanowy.data.db.entities.LuckyNumber +import io.github.wulkanowy.ui.base.BaseView +import java.time.LocalDate + +interface LuckyNumberHistoryView : BaseView { + + val isViewEmpty: Boolean + + fun initView() + + fun updateData(data: List) + + fun clearData() + + fun showEmpty(show: Boolean) + + fun showErrorView(show: Boolean) + + fun setErrorDetails(message: String) + + fun updateNavigationWeek(date: String) + + fun showProgress(show: Boolean) + + fun showPreButton(show: Boolean) + + fun showNextButton(show: Boolean) + + fun showDatePickerDialog(currentDate: LocalDate) + + fun showContent(show: Boolean) + + fun onDestroyView() +} 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 5d93c5944..5fda72106 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 @@ -11,15 +11,21 @@ import android.graphics.drawable.Icon import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.P import android.os.Bundle import android.view.Menu import android.view.MenuItem -import android.view.View +import android.view.ViewGroup import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.core.view.updateMargins import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat import com.aurelhubert.ahbottomnavigation.AHBottomNavigation.TitleState.ALWAYS_SHOW import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem import com.google.android.material.elevation.ElevationOverlayProvider @@ -27,6 +33,8 @@ import com.ncapdevi.fragnav.FragNavController import com.ncapdevi.fragnav.FragNavController.Companion.HIDE import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityMainBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog @@ -42,15 +50,18 @@ import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.UpdateHelper +import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.safelyPopFragments import io.github.wulkanowy.utils.setOnViewChangeListener import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint -class MainActivity : BaseActivity(), MainView { +class MainActivity : BaseActivity(), MainView, + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { @Inject override lateinit var presenter: MainPresenter @@ -64,6 +75,8 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo + private var accountMenu: MenuItem? = null + private val overlayProvider by lazy { ElevationOverlayProvider(this) } private val navController = @@ -121,6 +134,11 @@ class MainActivity : BaseActivity(), MainVie initialize(startMenuIndex, savedInstanceState) pushFragment(moreMenuFragments[startMenuMoreIndex]) } + + if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) { + initShortcuts() + } + updateHelper.checkAndInstallUpdates(this) } @@ -129,11 +147,11 @@ class MainActivity : BaseActivity(), MainVie updateHelper.onResume(this) } - @SuppressLint("NewApi") + //https://developer.android.com/guide/playcore/in-app-updates#status_callback + @Suppress("DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) updateHelper.onActivityResult(requestCode, resultCode) - if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) initShortcuts() } @RequiresApi(Build.VERSION_CODES.N_MR1) @@ -160,11 +178,6 @@ class MainActivity : BaseActivity(), MainVie getString(R.string.timetable_title), R.drawable.ic_shortcut_timetable, MainView.Section.TIMETABLE - ), - Triple( - getString(R.string.message_title), - R.drawable.ic_shortcut_message, - MainView.Section.MESSAGE ) ).forEach { (title, icon, enum) -> shortcutsList.add( @@ -191,9 +204,13 @@ class MainActivity : BaseActivity(), MainVie override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.action_menu_main, menu) + accountMenu = menu?.findItem(R.id.mainMenuAccount) + + presenter.onActionMenuCreated() return true } + @SuppressLint("NewApi") override fun initView() { with(binding.mainToolbar) { if (SDK_INT >= LOLLIPOP) stateListAnimator = null @@ -233,13 +250,27 @@ class MainActivity : BaseActivity(), MainVie with(navController) { setOnViewChangeListener { section, name -> - binding.mainBottomNav.visibility = - if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) { - View.GONE - } else { - View.VISIBLE + if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) { + binding.mainBottomNav.isVisible = false + binding.mainFragmentContainer.updateLayoutParams { + updateMargins(bottom = 0) } + if (appInfo.systemVersion >= P) { + window.navigationBarColor = getThemeAttrColor(R.attr.colorSurface) + } + } else { + binding.mainBottomNav.isVisible = true + binding.mainFragmentContainer.updateLayoutParams { + updateMargins(bottom = dpToPx(56f).toInt()) + } + + if (appInfo.systemVersion >= P) { + window.navigationBarColor = + getThemeAttrColor(android.R.attr.navigationBarColor) + } + } + analytics.setCurrentScreen(this@MainActivity, name) presenter.onViewChange(section) } @@ -254,6 +285,16 @@ class MainActivity : BaseActivity(), MainVie } } + override fun onPreferenceStartFragment( + caller: PreferenceFragmentCompat, + pref: Preference + ): Boolean { + val fragment = + supportFragmentManager.fragmentFactory.instantiate(classLoader, pref.fragment) + pushView(fragment) + return true + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { return if (item.itemId == R.id.mainMenuAccount) presenter.onAccountManagerSelected() else false @@ -264,6 +305,8 @@ class MainActivity : BaseActivity(), MainVie } override fun switchMenuView(position: Int) { + if (supportFragmentManager.isStateSaved) return + analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName) navController.switchTab(position) } @@ -280,8 +323,10 @@ class MainActivity : BaseActivity(), MainVie supportActionBar?.setDisplayHomeAsUpEnabled(show) } - override fun showAccountPicker() { - navController.showDialogFragment(AccountQuickDialog.newInstance()) + override fun showAccountPicker(studentWithSemesters: List) { + if (supportFragmentManager.isStateSaved) return + + navController.showDialogFragment(AccountQuickDialog.newInstance(studentWithSemesters)) } override fun showActionBarElevation(show: Boolean) { @@ -298,15 +343,21 @@ class MainActivity : BaseActivity(), MainVie } fun showDialogFragment(dialog: DialogFragment) { + if (supportFragmentManager.isStateSaved) return + navController.showDialogFragment(dialog) } fun pushView(fragment: Fragment) { + if (supportFragmentManager.isStateSaved) return + analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName) navController.pushFragment(fragment) } override fun popView(depth: Int) { + if (supportFragmentManager.isStateSaved) return + analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName) navController.safelyPopFragments(depth) } @@ -315,6 +366,13 @@ class MainActivity : BaseActivity(), MainVie presenter.onBackPressed { super.onBackPressed() } } + override fun showStudentAvatar(student: Student) { + accountMenu?.run { + icon = createNameInitialsDrawable(student.nickOrName, student.avatarColor, 0.44f) + title = getString(R.string.main_account_picker) + } + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) 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 59937b332..ffcfcb614 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 @@ -1,5 +1,7 @@ package io.github.wulkanowy.ui.modules.main +import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.sync.SyncManager @@ -9,6 +11,8 @@ import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.flowWithResource +import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -17,9 +21,11 @@ class MainPresenter @Inject constructor( studentRepository: StudentRepository, private val prefRepository: PreferencesRepository, private val syncManager: SyncManager, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, ) : BasePresenter(errorHandler, studentRepository) { + var studentsWitSemesters: List? = null + fun onAttachView(view: MainView, initMenu: MainView.Section?) { super.onAttachView(view) view.apply { @@ -35,6 +41,28 @@ class MainPresenter @Inject constructor( analytics.logEvent("app_open", "destination" to initMenu?.name) } + fun onActionMenuCreated() { + if (!studentsWitSemesters.isNullOrEmpty()) { + showCurrentStudentAvatar() + return + } + + flowWithResource { studentRepository.getSavedStudents(false) } + .onEach { resource -> + when (resource.status) { + Status.LOADING -> Timber.i("Loading student avatar data started") + Status.SUCCESS -> { + studentsWitSemesters = resource.data + showCurrentStudentAvatar() + } + Status.ERROR -> { + Timber.i("Loading student avatar result: An exception occurred") + errorHandler.dispatch(resource.error!!) + } + } + }.launch("avatar") + } + fun onViewChange(section: MainView.Section?) { view?.apply { showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) @@ -48,8 +76,10 @@ class MainPresenter @Inject constructor( } fun onAccountManagerSelected(): Boolean { + if (studentsWitSemesters.isNullOrEmpty()) return true + Timber.i("Select account manager") - view?.showAccountPicker() + view?.showAccountPicker(studentsWitSemesters!!) return true } @@ -81,6 +111,13 @@ class MainPresenter @Inject constructor( } == true } + private fun showCurrentStudentAvatar() { + val currentStudent = + studentsWitSemesters?.singleOrNull { it.student.isCurrent }?.student ?: return + + view?.showStudentAvatar(currentStudent) + } + private fun getProperViewIndexes(initMenu: MainView.Section?): Pair { return when (initMenu?.id) { in 0..3 -> initMenu!!.id to -1 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 7f4098147..a4b7d094b 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 @@ -1,5 +1,7 @@ package io.github.wulkanowy.ui.modules.main +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView interface MainView : BaseView { @@ -22,7 +24,7 @@ interface MainView : BaseView { fun showHomeArrow(show: Boolean) - fun showAccountPicker() + fun showAccountPicker(studentWithSemesters: List) fun showActionBarElevation(show: Boolean) @@ -36,6 +38,8 @@ interface MainView : BaseView { fun popView(depth: Int = 1) + fun showStudentAvatar(student: Student) + interface MainChildView { fun onFragmentReselected() 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 48150d8de..6357c495a 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 @@ -20,13 +20,15 @@ import io.github.wulkanowy.ui.base.BaseDialogFragment import javax.inject.Inject @AndroidEntryPoint -class MobileDeviceTokenDialog : BaseDialogFragment(), MobileDeviceTokenVIew { +class MobileDeviceTokenDialog : BaseDialogFragment(), + MobileDeviceTokenVIew { @Inject lateinit var presenter: MobileDeviceTokenPresenter companion object { - fun newInstance(): MobileDeviceTokenDialog = MobileDeviceTokenDialog() + + fun newInstance() = MobileDeviceTokenDialog() } override fun onCreate(savedInstanceState: Bundle?) { @@ -34,12 +36,14 @@ class MobileDeviceTokenDialog : BaseDialogFragment(), setStyle(STYLE_NO_TITLE, 0) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) presenter.onAttachView(this) } 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 bf8918fce..c4a14a521 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 @@ -63,9 +63,6 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), override val settingsRes: Pair? get() = context?.run { getString(R.string.settings_title) to getCompatDrawable(R.drawable.ic_more_settings) } - 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) @@ -124,10 +121,6 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), (activity as? MainActivity)?.pushView(SettingsFragment.newInstance()) } - override fun openAboutView() { - (activity as? MainActivity)?.pushView(AboutFragment.newInstance()) - } - override fun popView(depth: Int) { (activity as? MainActivity)?.popView(depth) } 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 d119000d3..6079141f4 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 @@ -30,7 +30,6 @@ class MorePresenter @Inject constructor( conferencesRes?.first -> openConferencesView() schoolAndTeachersRes?.first -> openSchoolAndTeachersView() settingsRes?.first -> openSettingsView() - aboutRes?.first -> openAboutView() } } } @@ -51,8 +50,7 @@ class MorePresenter @Inject constructor( mobileDevicesRes, conferencesRes, schoolAndTeachersRes, - settingsRes, - aboutRes + settingsRes )) } } 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 bb1faeda4..0543742e4 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 @@ -21,16 +21,12 @@ interface MoreView : BaseView { val settingsRes: Pair? - val aboutRes: Pair? - fun initView() fun updateData(data: List>) fun openSettingsView() - fun openAboutView() - fun popView(depth: Int) fun openMessagesView() 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 e016fe0b9..4cb1cda02 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 @@ -22,12 +22,11 @@ class NoteDialog : DialogFragment() { private lateinit var note: Note companion object { + private const val ARGUMENT_KEY = "Item" - fun newInstance(exam: Note): NoteDialog { - return NoteDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } - } + fun newInstance(exam: Note) = NoteDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } } } @@ -39,13 +38,15 @@ class NoteDialog : DialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogNoteBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogNoteBinding.inflate(inflater).apply { binding = this }.root @SuppressLint("SetTextI18n") - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) with(binding) { noteDialogDate.text = note.date.toFormattedString() @@ -57,11 +58,19 @@ class NoteDialog : DialogFragment() { if (note.isPointsShow) { with(binding.noteDialogPoints) { text = "${if (note.points > 0) "+" else ""}${note.points}" - setTextColor(when (NoteCategory.getByValue(note.categoryType)) { - NoteCategory.POSITIVE -> ContextCompat.getColor(requireContext(), R.color.note_positive) - NoteCategory.NEGATIVE -> ContextCompat.getColor(requireContext(), R.color.note_negative) - else -> requireContext().getThemeAttrColor(android.R.attr.textColorPrimary) - }) + setTextColor( + when (NoteCategory.getByValue(note.categoryType)) { + NoteCategory.POSITIVE -> ContextCompat.getColor( + requireContext(), + R.color.note_positive + ) + NoteCategory.NEGATIVE -> ContextCompat.getColor( + requireContext(), + R.color.note_negative + ) + else -> requireContext().getThemeAttrColor(android.R.attr.textColorPrimary) + } + ) } } 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 03ad7ba09..fba2f0407 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 @@ -53,13 +53,14 @@ class SchoolFragment : BaseFragment(R.layout.fragment_sch override fun updateData(data: School) { with(binding) { - schoolName.text = data.name - schoolAddress.text = data.address.ifBlank { "-" } + val noDataString = getString(R.string.all_no_data) + schoolName.text = data.name.ifBlank { noDataString } + schoolAddress.text = data.address.ifBlank { noDataString } schoolAddressButton.visibility = if (data.address.isNotBlank()) VISIBLE else GONE - schoolTelephone.text = data.contact.ifBlank { "-" } + schoolTelephone.text = data.contact.ifBlank { noDataString } schoolTelephoneButton.visibility = if (data.contact.isNotBlank()) VISIBLE else GONE - schoolHeadmaster.text = data.headmaster - schoolPedagogue.text = data.pedagogue + schoolHeadmaster.text = data.headmaster.ifBlank { noDataString } + schoolPedagogue.text = data.pedagogue.ifBlank { noDataString } } } 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 ad4692b96..2612fab3a 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 @@ -1,148 +1,22 @@ package io.github.wulkanowy.ui.modules.settings -import android.content.SharedPreferences 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.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -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 +import timber.log.Timber -@AndroidEntryPoint -class SettingsFragment : PreferenceFragmentCompat(), - SharedPreferences.OnSharedPreferenceChangeListener, - MainView.TitledView, SettingsView { - - @Inject - lateinit var presenter: SettingsPresenter - - @Inject - lateinit var appInfo: AppInfo - - @Inject - lateinit var lingver: Lingver +class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { companion object { + fun newInstance() = SettingsFragment() } override val titleStringId get() = R.string.settings_title - override val syncSuccessString get() = getString(R.string.pref_services_message_sync_success) - - override val syncFailedString get() = getString(R.string.pref_services_message_sync_failed) - - override fun initView() { - findPreference(getString(R.string.pref_key_services_force_sync))?.run { - onPreferenceClickListener = Preference.OnPreferenceClickListener { - presenter.onSyncNowClicked() - 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?) { - super.onActivityCreated(savedInstanceState) - presenter.onAttachView(this) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.scheme_preferences, rootKey) - findPreference(getString(R.string.pref_key_notification_debug))?.isVisible = appInfo.isDebug - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - presenter.onSharedPreferenceChanged(key) - } - - override fun recreateView() { - activity?.recreate() - } - - override fun updateLanguage(langCode: String) { - lingver.setLocale(requireContext(), langCode) - } - - override fun updateLanguageToFollowSystem() { - lingver.setFollowSystemLocale(requireContext()) - } - - override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) { - findPreference(serviceEnablesKey)?.run { - summary = if (isHolidays) getString(R.string.pref_services_suspended) else "" - isEnabled = !isHolidays - } - } - - override fun setSyncInProgress(inProgress: Boolean) { - if (activity == null || !isAdded) return - - findPreference(getString(R.string.pref_key_services_force_sync))?.run { - isEnabled = !inProgress - summary = if (inProgress) getString(R.string.pref_services_sync_in_progress) else "" - } - } - - override fun showError(text: String, error: Throwable) { - (activity as? BaseActivity<*, *>)?.showError(text, error) - } - - override fun showMessage(text: String) { - (activity as? BaseActivity<*, *>)?.showMessage(text) - } - - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() - } - - override fun openClearLoginView() { - (activity as? BaseActivity<*, *>)?.openClearLoginView() - } - - override fun showErrorDetailsDialog(error: Throwable) { - ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) - } - - 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) - } - - override fun onPause() { - super.onPause() - preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + Timber.i("Settings view was initialized") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt new file mode 100644 index 000000000..524d7ba6d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -0,0 +1,78 @@ +package io.github.wulkanowy.ui.modules.settings.advanced + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.preference.PreferenceFragmentCompat +import com.yariksoffice.lingver.Lingver +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +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 javax.inject.Inject + +@AndroidEntryPoint +class AdvancedFragment : PreferenceFragmentCompat(), + SharedPreferences.OnSharedPreferenceChangeListener, + MainView.TitledView, AdvancedView { + + @Inject + lateinit var presenter: AdvancedPresenter + + @Inject + lateinit var appInfo: AppInfo + + @Inject + lateinit var lingver: Lingver + + companion object { + fun newInstance() = AdvancedFragment() + } + + override val titleStringId get() = R.string.pref_settings_advanced_title + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.scheme_preferences_advanced, rootKey) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + presenter.onSharedPreferenceChanged(key) + } + + override fun showError(text: String, error: Throwable) { + (activity as? BaseActivity<*, *>)?.showError(text, error) + } + + override fun showMessage(text: String) { + (activity as? BaseActivity<*, *>)?.showMessage(text) + } + + override fun showExpiredDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredDialog() + } + + override fun openClearLoginView() { + (activity as? BaseActivity<*, *>)?.openClearLoginView() + } + + override fun showErrorDetailsDialog(error: Throwable) { + ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) + } + + override fun onResume() { + super.onResume() + preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt new file mode 100644 index 000000000..d38f841fe --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt @@ -0,0 +1,25 @@ +package io.github.wulkanowy.ui.modules.settings.advanced + +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AnalyticsHelper +import timber.log.Timber +import javax.inject.Inject + +class AdvancedPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val analytics: AnalyticsHelper, +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: AdvancedView) { + super.onAttachView(view) + Timber.i("Settings advanced view was initialized") + } + + fun onSharedPreferenceChanged(key: String) { + Timber.i("Change settings $key") + analytics.logEvent("setting_changed", "name" to key) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedView.kt new file mode 100644 index 000000000..c2e8e52dc --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedView.kt @@ -0,0 +1,5 @@ +package io.github.wulkanowy.ui.modules.settings.advanced + +import io.github.wulkanowy.ui.base.BaseView + +interface AdvancedView : BaseView {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt new file mode 100644 index 000000000..5ac801dc6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -0,0 +1,90 @@ +package io.github.wulkanowy.ui.modules.settings.appearance + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.preference.PreferenceFragmentCompat +import com.yariksoffice.lingver.Lingver +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +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 javax.inject.Inject + +@AndroidEntryPoint +class AppearanceFragment : PreferenceFragmentCompat(), + SharedPreferences.OnSharedPreferenceChangeListener, + MainView.TitledView, AppearanceView { + + @Inject + lateinit var presenter: AppearancePresenter + + @Inject + lateinit var appInfo: AppInfo + + @Inject + lateinit var lingver: Lingver + + companion object { + fun newInstance() = AppearanceFragment() + } + + override val titleStringId get() = R.string.pref_settings_appearance_title + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + presenter.onSharedPreferenceChanged(key) + } + + override fun recreateView() { + activity?.recreate() + } + + override fun updateLanguage(langCode: String) { + lingver.setLocale(requireContext(), langCode) + } + + override fun updateLanguageToFollowSystem() { + lingver.setFollowSystemLocale(requireContext()) + } + + override fun showError(text: String, error: Throwable) { + (activity as? BaseActivity<*, *>)?.showError(text, error) + } + + override fun showMessage(text: String) { + (activity as? BaseActivity<*, *>)?.showMessage(text) + } + + override fun showExpiredDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredDialog() + } + + override fun openClearLoginView() { + (activity as? BaseActivity<*, *>)?.openClearLoginView() + } + + override fun showErrorDetailsDialog(error: Throwable) { + ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) + } + + override fun onResume() { + super.onResume() + preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt new file mode 100644 index 000000000..14592a6cc --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt @@ -0,0 +1,45 @@ +package io.github.wulkanowy.ui.modules.settings.appearance + +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.AppInfo +import timber.log.Timber +import javax.inject.Inject + +class AppearancePresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val preferencesRepository: PreferencesRepository, + private val analytics: AnalyticsHelper, + private val appInfo: AppInfo +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: AppearanceView) { + super.onAttachView(view) + Timber.i("Settings appearance view was initialized") + } + + fun onSharedPreferenceChanged(key: String) { + Timber.i("Change settings $key") + + preferencesRepository.apply { + when (key) { + appThemeKey -> view?.recreateView() + appLanguageKey -> view?.run { + if (appLanguage == "system") { + updateLanguageToFollowSystem() + analytics.logEvent("language", "setting_changed" to appInfo.systemLanguage) + } else { + updateLanguage(appLanguage) + analytics.logEvent("language", "setting_changed" to appLanguage) + } + recreateView() + } + } + } + analytics.logEvent("setting_changed", "name" to key) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceView.kt new file mode 100644 index 000000000..ecee4f423 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceView.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.ui.modules.settings.appearance + +import io.github.wulkanowy.ui.base.BaseView + +interface AppearanceView : BaseView { + + fun recreateView() + + fun updateLanguage(langCode: String) + + fun updateLanguageToFollowSystem() +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt new file mode 100644 index 000000000..a9641d887 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -0,0 +1,130 @@ +package io.github.wulkanowy.ui.modules.settings.notifications + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.recyclerview.widget.RecyclerView +import com.thelittlefireman.appkillermanager.AppKillerManager +import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +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.openInternetBrowser +import javax.inject.Inject + +@AndroidEntryPoint +class NotificationsFragment : PreferenceFragmentCompat(), + SharedPreferences.OnSharedPreferenceChangeListener, + MainView.TitledView, NotificationsView { + + @Inject + lateinit var presenter: NotificationsPresenter + + companion object { + fun newInstance() = NotificationsFragment() + } + + override val titleStringId get() = R.string.pref_settings_notifications_title + + override fun initView(showDebugNotificationSwitch: Boolean) { + findPreference(getString(R.string.pref_key_notification_debug))?.isVisible = + showDebugNotificationSwitch + + findPreference(getString(R.string.pref_key_notifications_fix_issues))?.run { + isVisible = AppKillerManager.isDeviceSupported() + && AppKillerManager.isAnyActionAvailable(requireContext()) + + setOnPreferenceClickListener { + presenter.onFixSyncIssuesClicked() + true + } + } + } + + override fun onCreateRecyclerView( + inflater: LayoutInflater?, + parent: ViewGroup?, + state: Bundle? + ): RecyclerView? = super.onCreateRecyclerView(inflater, parent, state) + .also { + it.itemAnimator = null + it.layoutAnimation = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.scheme_preferences_notifications, rootKey) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + presenter.onSharedPreferenceChanged(key) + } + + override fun enableNotification(notificationKey: String, enable: Boolean) { + findPreference(notificationKey)?.run { + isEnabled = enable + summary = if (enable) null else getString(R.string.pref_notify_disabled_summary) + } + } + + override fun showError(text: String, error: Throwable) { + (activity as? BaseActivity<*, *>)?.showError(text, error) + } + + override fun showMessage(text: String) { + (activity as? BaseActivity<*, *>)?.showMessage(text) + } + + override fun showExpiredDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredDialog() + } + + override fun openClearLoginView() { + (activity as? BaseActivity<*, *>)?.openClearLoginView() + } + + override fun showErrorDetailsDialog(error: Throwable) { + ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) + } + + 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) + } + + override fun onPause() { + super.onPause() + preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt new file mode 100644 index 000000000..981af17de --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt @@ -0,0 +1,58 @@ +package io.github.wulkanowy.ui.modules.settings.notifications + +import com.chuckerteam.chucker.api.ChuckerCollector +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.AppInfo +import timber.log.Timber +import javax.inject.Inject + +class NotificationsPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val preferencesRepository: PreferencesRepository, + private val timetableNotificationHelper: TimetableNotificationSchedulerHelper, + private val appInfo: AppInfo, + private val analytics: AnalyticsHelper, + private val chuckerCollector: ChuckerCollector +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: NotificationsView) { + super.onAttachView(view) + + with(view) { + enableNotification( + preferencesRepository.notificationsEnableKey, + preferencesRepository.isServiceEnabled + ) + initView(appInfo.isDebug) + } + Timber.i("Settings notifications view was initialized") + } + + fun onSharedPreferenceChanged(key: String) { + Timber.i("Change settings $key") + + preferencesRepository.apply { + when (key) { + isUpcomingLessonsNotificationsEnableKey -> { + if (!isUpcomingLessonsNotificationsEnable) { + timetableNotificationHelper.cancelNotification() + } + } + isDebugNotificationEnableKey -> { + chuckerCollector.showNotification = isDebugNotificationEnable + } + } + } + analytics.logEvent("setting_changed", "name" to key) + } + + fun onFixSyncIssuesClicked() { + view?.showFixSyncDialog() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt new file mode 100644 index 000000000..2618cde1e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.ui.modules.settings.notifications + +import io.github.wulkanowy.ui.base.BaseView + +interface NotificationsView : BaseView { + + fun initView(showDebugNotificationSwitch: Boolean) + + fun showFixSyncDialog() + + fun enableNotification(notificationKey: String, enable: Boolean) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt new file mode 100644 index 000000000..207fe2ff6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -0,0 +1,100 @@ +package io.github.wulkanowy.ui.modules.settings.sync + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.main.MainView +import javax.inject.Inject + +@AndroidEntryPoint +class SyncFragment : PreferenceFragmentCompat(), + SharedPreferences.OnSharedPreferenceChangeListener, + MainView.TitledView, SyncView { + + @Inject + lateinit var presenter: SyncPresenter + + companion object { + fun newInstance() = SyncFragment() + } + + override val titleStringId get() = R.string.pref_settings_sync_title + + override val syncSuccessString get() = getString(R.string.pref_services_message_sync_success) + + override val syncFailedString get() = getString(R.string.pref_services_message_sync_failed) + + override fun initView() { + findPreference(getString(R.string.pref_key_services_force_sync))?.run { + onPreferenceClickListener = Preference.OnPreferenceClickListener { + presenter.onSyncNowClicked() + true + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.scheme_preferences_sync, rootKey) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + presenter.onSharedPreferenceChanged(key) + } + + override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) { + findPreference(serviceEnablesKey)?.run { + summary = if (isHolidays) getString(R.string.pref_services_suspended) else "" + isEnabled = !isHolidays + } + } + + override fun setSyncInProgress(inProgress: Boolean) { + if (activity == null || !isAdded) return + + findPreference(getString(R.string.pref_key_services_force_sync))?.run { + isEnabled = !inProgress + summary = if (inProgress) getString(R.string.pref_services_sync_in_progress) else "" + } + } + + override fun showError(text: String, error: Throwable) { + (activity as? BaseActivity<*, *>)?.showError(text, error) + } + + override fun showMessage(text: String) { + (activity as? BaseActivity<*, *>)?.showMessage(text) + } + + override fun showExpiredDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredDialog() + } + + override fun openClearLoginView() { + (activity as? BaseActivity<*, *>)?.openClearLoginView() + } + + override fun showErrorDetailsDialog(error: Throwable) { + ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) + } + + override fun onResume() { + super.onResume() + preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(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/sync/SyncPresenter.kt similarity index 63% rename from app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt index e3b2e232f..36b8d792c 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/sync/SyncPresenter.kt @@ -1,15 +1,12 @@ -package io.github.wulkanowy.ui.modules.settings +package io.github.wulkanowy.ui.modules.settings.sync import androidx.work.WorkInfo -import com.chuckerteam.chucker.api.ChuckerCollector import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.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 import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.isHolidays import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach @@ -17,20 +14,17 @@ import timber.log.Timber import java.time.LocalDate.now import javax.inject.Inject -class SettingsPresenter @Inject constructor( +class SyncPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val preferencesRepository: PreferencesRepository, - private val timetableNotificationHelper: TimetableNotificationSchedulerHelper, private val analytics: AnalyticsHelper, private val syncManager: SyncManager, - private val chuckerCollector: ChuckerCollector, - private val appInfo: AppInfo -) : BasePresenter(errorHandler, studentRepository) { +) : BasePresenter(errorHandler, studentRepository) { - override fun onAttachView(view: SettingsView) { + override fun onAttachView(view: SyncView) { super.onAttachView(view) - Timber.i("Settings view was initialized") + Timber.i("Settings sync view was initialized") view.setServicesSuspended(preferencesRepository.serviceEnableKey, now().isHolidays) view.initView() } @@ -42,20 +36,6 @@ class SettingsPresenter @Inject constructor( 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 { - if (appLanguage == "system") { - updateLanguageToFollowSystem() - analytics.logEvent("language", "setting_changed" to appInfo.systemLanguage) - } else { - updateLanguage(appLanguage) - analytics.logEvent("language", "setting_changed" to appLanguage) - } - recreateView() - } } } analytics.logEvent("setting_changed", "name" to key) @@ -89,8 +69,4 @@ class SettingsPresenter @Inject constructor( }.launch("sync") } } - - fun onFixSyncIssuesClicked() { - view?.showFixSyncDialog() - } } 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/sync/SyncView.kt similarity index 54% rename from app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncView.kt index 802717412..9da473ba7 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/sync/SyncView.kt @@ -1,8 +1,8 @@ -package io.github.wulkanowy.ui.modules.settings +package io.github.wulkanowy.ui.modules.settings.sync import io.github.wulkanowy.ui.base.BaseView -interface SettingsView : BaseView { +interface SyncView : BaseView { val syncSuccessString: String @@ -10,15 +10,7 @@ interface SettingsView : BaseView { fun initView() - fun recreateView() - - fun updateLanguage(langCode: String) - - fun updateLanguageToFollowSystem() - fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) fun setSyncInProgress(inProgress: Boolean) - - fun showFixSyncDialog() } 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 f7d5b1ed9..7744515d9 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 @@ -24,12 +24,11 @@ class TimetableDialog : DialogFragment() { private lateinit var lesson: Timetable companion object { + private const val ARGUMENT_KEY = "Item" - fun newInstance(exam: Timetable): TimetableDialog { - return TimetableDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } - } + fun newInstance(exam: Timetable) = TimetableDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } } } @@ -41,12 +40,14 @@ class TimetableDialog : DialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogTimetableBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogTimetableBinding.inflate(inflater).apply { binding = this }.root - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) with(lesson) { setInfo(info, teacher, canceled, changes) @@ -76,15 +77,24 @@ class TimetableDialog : DialogFragment() { } } + @SuppressLint("DefaultLocale") 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)) + timetableDialogChangesTitle.setTextColor( + requireContext().getThemeAttrColor( + R.attr.colorPrimary + ) + ) timetableDialogChanges.setTextColor(requireContext().getThemeAttrColor(R.attr.colorPrimary)) } else { - timetableDialogChangesTitle.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange)) + timetableDialogChangesTitle.setTextColor( + requireContext().getThemeAttrColor( + R.attr.colorTimetableChange + ) + ) timetableDialogChanges.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange)) } @@ -167,6 +177,7 @@ class TimetableDialog : DialogFragment() { @SuppressLint("SetTextI18n") private fun setTime(start: LocalDateTime, end: LocalDateTime) { - binding.timetableDialogTime.text = "${start.toFormattedString("HH:mm")} - ${end.toFormattedString("HH:mm")}" + binding.timetableDialogTime.text = + "${start.toFormattedString("HH:mm")} - ${end.toFormattedString("HH:mm")}" } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt index 56ea16cfa..258fc59df 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 @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.timetable.completed -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -17,12 +16,11 @@ class CompletedLessonDialog : DialogFragment() { private lateinit var completedLesson: CompletedLesson companion object { + private const val ARGUMENT_KEY = "Item" - fun newInstance(exam: CompletedLesson): CompletedLessonDialog { - return CompletedLessonDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } - } + fun newInstance(exam: CompletedLesson) = CompletedLessonDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } } } @@ -34,13 +32,14 @@ class CompletedLessonDialog : DialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return DialogLessonCompletedBinding.inflate(inflater).apply { binding = this }.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogLessonCompletedBinding.inflate(inflater).apply { binding = this }.root - @SuppressLint("SetTextI18n") - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) with(binding) { completedLessonDialogSubject.text = completedLesson.subject 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 82671a7f4..a3961aed8 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -35,8 +35,8 @@ open class AppInfo @Inject constructor() { open val systemLanguage: String get() = Resources.getSystem().configuration.locale.language - open val defaultColorsForAvatar = listOf( - 0xe57373, 0xf06292, 0xba68c8, 0x9575cd, 0x7986cb, 0x64b5f6, 0x4fc3f7, 0x4dd0e1, 0x4db6ac, - 0x81c784, 0xaed581, 0xff8a65, 0xd4e157, 0xffd54f, 0xffb74d, 0xa1887f, 0x90a4ae - ) + val defaultColorsForAvatar = listOf( + 0xd32f2f, 0xE64A19, 0xFFA000, 0xAFB42B, 0x689F38, 0x388E3C, 0x00796B, 0x0097A7, + 0x1976D2, 0x3647b5, 0x6236c9, 0x9225c1, 0xC2185B, 0x616161, 0x455A64, 0x7a5348 + ).map { (it and 0x00ffffff or (255 shl 24)).toLong() } } 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..92c0ddfc5 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -1,8 +1,15 @@ package io.github.wulkanowy.utils +import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Typeface import android.net.Uri +import android.text.TextPaint import android.util.DisplayMetrics.DENSITY_DEFAULT import androidx.annotation.AttrRes import androidx.annotation.ColorInt @@ -10,6 +17,9 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils +import androidx.core.graphics.applyCanvas +import androidx.core.graphics.drawable.RoundedBitmapDrawable +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import io.github.wulkanowy.BuildConfig.APPLICATION_ID @ColorInt @@ -30,7 +40,8 @@ fun Context.getThemeAttrColor(@AttrRes colorAttr: Int, alpha: Int): Int { @ColorInt fun Context.getCompatColor(@ColorRes colorRes: Int) = ContextCompat.getColor(this, colorRes) -fun Context.getCompatDrawable(@DrawableRes drawableRes: Int) = ContextCompat.getDrawable(this, drawableRes) +fun Context.getCompatDrawable(@DrawableRes drawableRes: Int) = + ContextCompat.getDrawable(this, drawableRes) fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit) { Intent.parseUri(uri, 0).let { @@ -45,7 +56,13 @@ fun Context.openAppInMarket(onActivityNotFound: (uri: String) -> Unit) { } } -fun Context.openEmailClient(chooserTitle: String, email: String, subject: String, body: String, onActivityNotFound: () -> Unit = {}) { +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)) putExtra(Intent.EXTRA_SUBJECT, subject) @@ -85,3 +102,39 @@ fun Context.shareText(text: String, subject: String?) { } fun Context.dpToPx(dp: Float) = dp * resources.displayMetrics.densityDpi / DENSITY_DEFAULT + +@SuppressLint("DefaultLocale") +fun Context.createNameInitialsDrawable( + text: String, + backgroundColor: Long, + scaleFactory: Float = 1f +): RoundedBitmapDrawable { + val words = text.split(" ") + val firstCharFirstWord = words.getOrNull(0)?.firstOrNull() ?: "" + val firstCharSecondWord = words.getOrNull(1)?.firstOrNull() ?: "" + + val initials = "$firstCharFirstWord$firstCharSecondWord".toUpperCase() + + val bounds = Rect() + val dimension = this.dpToPx(64f * scaleFactory).toInt() + val textPaint = TextPaint().apply { + typeface = Typeface.SANS_SERIF + color = Color.WHITE + textAlign = Paint.Align.CENTER + isAntiAlias = true + textSize = this@createNameInitialsDrawable.dpToPx(30f * scaleFactory) + getTextBounds(initials, 0, initials.length, bounds) + } + + val xCoordinate = (dimension / 2).toFloat() + val yCoordinate = (dimension / 2 + (bounds.bottom - bounds.top) / 2).toFloat() + + val bitmap = Bitmap.createBitmap(dimension, dimension, Bitmap.Config.ARGB_8888) + .applyCanvas { + drawColor(backgroundColor.toInt()) + drawText(initials, 0, initials.length, xCoordinate, yCoordinate, textPaint) + } + + return RoundedBitmapDrawableFactory.create(this.resources, bitmap) + .apply { isCircular = true } +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt index 049e1d42a..5dd289677 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt @@ -13,8 +13,11 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock inline fun networkBoundResource( + mutex: Mutex = Mutex(), showSavedOnLoading: Boolean = true, crossinline query: () -> Flow, crossinline fetch: suspend (ResultType) -> RequestType, @@ -31,7 +34,7 @@ inline fun networkBoundResource( try { val newData = fetch(data) - saveFetchResult(data, newData) + mutex.withLock { saveFetchResult(query().first(), newData) } query().map { Resource.success(filterResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) @@ -44,11 +47,12 @@ inline fun networkBoundResource( @JvmName("networkBoundResourceWithMap") inline fun networkBoundResource( + mutex: Mutex = Mutex(), showSavedOnLoading: Boolean = true, crossinline query: () -> Flow, crossinline fetch: suspend (ResultType) -> RequestType, crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline onFetchFailed: (Throwable) -> Unit = { Unit }, + crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline shouldFetch: (ResultType) -> Boolean = { true }, crossinline mapResult: (ResultType) -> T ) = flow { @@ -59,7 +63,8 @@ inline fun networkBoundResource( if (showSavedOnLoading) emit(Resource.loading(mapResult(data))) try { - saveFetchResult(data, fetch(data)) + val newData = fetch(data) + mutex.withLock { saveFetchResult(query().first(), newData) } query().map { Resource.success(mapResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) diff --git a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt index b96faeb21..d2a8908ce 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt @@ -1,5 +1,7 @@ package io.github.wulkanowy.utils +import android.os.Handler +import android.os.Looper import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle @@ -28,7 +30,8 @@ class LifecycleAwareVariable : ReadWriteProperty, Lifecycl } } -class LifecycleAwareVariableActivity : ReadWriteProperty, LifecycleObserver { +class LifecycleAwareVariableActivity : ReadWriteProperty, + LifecycleObserver { private var _value: T? = null @@ -44,11 +47,12 @@ class LifecycleAwareVariableActivity : ReadWriteProperty Fragment.lifecycleAwareVariable() = LifecycleAwareVariable() 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 3404b05b8..2b0af6e5a 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,6 @@ -Wersja 1.0.1 +Wersja 1.1.1 -- naprawiliśmy błąd podczas wysyłania wiadomości -- poprawiliśmy błędy w tłumaczeniach +- naprawiliśmy wyświetlanie planu lekcji +- naprawiliśmy kilka rzadkich problemów ze stabilnością Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases diff --git a/app/src/main/res/drawable/ic_all_about.xml b/app/src/main/res/drawable/ic_all_about.xml index 3dfbbda19..3868f85c6 100644 --- a/app/src/main/res/drawable/ic_all_about.xml +++ b/app/src/main/res/drawable/ic_all_about.xml @@ -2,6 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" + android:tint="?colorOnSurface" android:viewportHeight="24"> + + diff --git a/app/src/main/res/drawable/ic_settings_advanced.xml b/app/src/main/res/drawable/ic_settings_advanced.xml new file mode 100644 index 000000000..2fd7e5907 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_advanced.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_settings_appearance.xml b/app/src/main/res/drawable/ic_settings_appearance.xml new file mode 100644 index 000000000..afea27f27 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_appearance.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_settings_notifications.xml b/app/src/main/res/drawable/ic_settings_notifications.xml new file mode 100644 index 000000000..f4ff247f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_notifications.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_sync.xml b/app/src/main/res/drawable/ic_settings_sync.xml new file mode 100644 index 000000000..3697ac0ba --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_sync.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2f88ecc82..2ea0a4d39 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - + app:contentInsetStartWithNavigation="0dp" /> + android:layout_height="match_parent" + android:layout_marginTop="?actionBarSize" + android:layout_marginBottom="@dimen/bottom_navigation_height" /> - + android:layout_gravity="bottom" /> + diff --git a/app/src/main/res/layout/dialog_account_edit.xml b/app/src/main/res/layout/dialog_account_edit.xml index b65f85ac3..9f617e440 100644 --- a/app/src/main/res/layout/dialog_account_edit.xml +++ b/app/src/main/res/layout/dialog_account_edit.xml @@ -1,87 +1,125 @@ - - - - + android:layout_height="wrap_content"> - + - + - - + - + - - + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_account_quick.xml b/app/src/main/res/layout/dialog_account_quick.xml index da31d31d3..4095c91ae 100644 --- a/app/src/main/res/layout/dialog_account_quick.xml +++ b/app/src/main/res/layout/dialog_account_quick.xml @@ -2,7 +2,7 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_account_details.xml b/app/src/main/res/layout/fragment_account_details.xml index 1a0de147e..af9564b5e 100644 --- a/app/src/main/res/layout/fragment_account_details.xml +++ b/app/src/main/res/layout/fragment_account_details.xml @@ -21,9 +21,9 @@ android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" - android:visibility="invisible" + android:visibility="gone" tools:ignore="UseCompoundDrawables" - tools:visibility="visible"> + tools:visibility="gone"> + android:layout_height="match_parent" + android:visibility="gone" + tools:visibility="visible"> + tools:ignore="ContentDescription" + tools:src="@tools:sample/avatars" /> + + + android:orientation="vertical"> @@ -34,153 +34,101 @@ + android:layout_height="match_parent"> - + android:layout_height="match_parent"> + + + + + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + android:visibility="invisible" + tools:ignore="UseCompoundDrawables"> - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginTop="20dp" + android:gravity="center" + android:text="@string/grade_no_items" + android:textSize="20sp" /> - + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_lucky_number.xml b/app/src/main/res/layout/fragment_lucky_number.xml index f6de01e69..b2d4f40ab 100644 --- a/app/src/main/res/layout/fragment_lucky_number.xml +++ b/app/src/main/res/layout/fragment_lucky_number.xml @@ -67,6 +67,26 @@ android:textSize="20sp" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml index 95fdb0e8b..563e6b1e9 100644 --- a/app/src/main/res/layout/item_account.xml +++ b/app/src/main/res/layout/item_account.xml @@ -18,15 +18,27 @@ android:layout_height="40dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_all_account" tools:ignore="ContentDescription" - tools:tint="@color/colorPrimary" /> + tools:src="@tools:sample/avatars" /> + + + + + + + diff --git a/app/src/main/res/layout/item_grade_statistics_header.xml b/app/src/main/res/layout/item_grade_statistics_header.xml new file mode 100644 index 000000000..92f522ba4 --- /dev/null +++ b/app/src/main/res/layout/item_grade_statistics_header.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_grade_statistics_pie.xml b/app/src/main/res/layout/item_grade_statistics_pie.xml index 15992a37e..2ea91ecf9 100644 --- a/app/src/main/res/layout/item_grade_statistics_pie.xml +++ b/app/src/main/res/layout/item_grade_statistics_pie.xml @@ -23,5 +23,4 @@ android:layout_margin="10dp" android:background="?android:windowBackground" tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter" /> - diff --git a/app/src/main/res/layout/item_lucky_number_history.xml b/app/src/main/res/layout/item_lucky_number_history.xml new file mode 100644 index 000000000..79a0fcb29 --- /dev/null +++ b/app/src/main/res/layout/item_lucky_number_history.xml @@ -0,0 +1,43 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/action_menu_main.xml b/app/src/main/res/menu/action_menu_main.xml index 72bea25a2..219059391 100644 --- a/app/src/main/res/menu/action_menu_main.xml +++ b/app/src/main/res/menu/action_menu_main.xml @@ -1,11 +1,11 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + app:showAsAction="always" + tools:ignore="MenuTitle" /> diff --git a/app/src/main/res/values-cs-v29/preferences_values.xml b/app/src/main/res/values-cs-v29/preferences_values.xml new file mode 100644 index 000000000..25125a308 --- /dev/null +++ b/app/src/main/res/values-cs-v29/preferences_values.xml @@ -0,0 +1,9 @@ + + + + Motiv systému + Světlý + Tmavý + Černý (AMOLED) + + \ No newline at end of file diff --git a/app/src/main/res/values-cs-rCZ/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml similarity index 98% rename from app/src/main/res/values-cs-rCZ/preferences_values.xml rename to app/src/main/res/values-cs/preferences_values.xml index e70abcb16..7abbb10bc 100644 --- a/app/src/main/res/values-cs-rCZ/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -13,6 +13,7 @@ Українська Deutsch Čeština + Slovenčina 15 minut diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs/strings.xml similarity index 92% rename from app/src/main/res/values-cs-rCZ/strings.xml rename to app/src/main/res/values-cs/strings.xml index 02ee382a0..8c8ad7291 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -46,6 +46,7 @@ Token vypršel Neplatný e-mail Místo e-mailu použijte přiřazené přihlašovací údaje + Použijte přiřazené přihlašovací nebo e-mail v @%1$s Neplatný symbol Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ Toto pole je povinné @@ -191,7 +192,7 @@ Důvod nepřítomnosti (volitelný) Poslat - Nepřítomnost úspěšně omluvena! + Žádost o omluvu nepřítomnosti byla úspěšně odeslána! Musíte vybrat alespoň jednu nepřítomnost! Ospravedlnit @@ -313,6 +314,10 @@ Žádné informace o šťastném čísle Šťastné číslo pro dnešek Dnes je šťastným číslem: %d + Zobrazit historii + + Historie šťastných čísel + Žádné informace o šťastných číslech Mobilní přístup Žádná zařízení @@ -404,6 +409,7 @@ Přezdívka Přidat přezdívku + Vybrat barvu avataru Sdílet protokoly Obnovit @@ -438,21 +444,21 @@ Tmavý Motiv systému - Vzhled + Vzhled a chování aplikací Výchozí zobrazení Výpočet koncoročního průměru Vynutit průměrný výpočet podle aplikace - Zobrazit přítomnost v docházce - Motiv aplikace + Zobrazit přítomnost + Motiv Rozbalit známky - Označit aktuální lekci v plánu lekce - Zobrazit skupiny vedle předmětů v plánu lekce + Označit aktuální lekci + Zobrazit skupiny vedle předmětů Zobrazit seznam grafů v známkách třídy Zobrazit lekce pro celou třídu - Zobrazit předměty bez známek v \"Známky\" + Zobrazit předměty bez známek Známky barevné schéma - Třídění předmětů v \"Známky\" - Jazyk aplikace + Třídění předmětů + Jazyk Upozornění Zobrazit upozornění Zobrazit upozornění o nadcházející lekci @@ -460,6 +466,7 @@ Vaše zařízení může mít problémy se synchronizací dat as upozorněními.\n\nChcete-li je opravit, přidejte Wulkanového do funkce Autostart a vypněte optimalizaci/úsporu baterie v nastavení systému telefonu. Přejít do nastavení Zobrazit upozornění o ladění + Synchronizace je vypnutá Synchronizace Automatická aktualizace Pozastaveno na dovolené @@ -469,14 +476,27 @@ Synchronizováno! Synchronizace selhala Probíhá synchronizace - Synchronizace - Ruční synchronizace neobnoví zobrazení aplikace. - \nChcete-li zobrazit synchronizovaná data, restartujte aplikaci po synchronizaci. - - Jiné Hodnota plusu Hodnota mínusu Odpovědět s historií zpráv + Pokročilé + Vzhled a chování + Upozornění + Synchronizace + Známky + Docházka + Plán lekce + Známky + Zprávy + Vzhled a chování + Jazyky, motivy, třídění předmětů + Upozornění aplikace, oprava problémů + Upozornění + Synchronizace + Automatická aktualizace, interval aktualizací + Hodnota plusu a mínusu, výpočet průměru + Pokročilé + Verze aplikace, tvůrci, sociální portály, licence Nové položky v deníku Nové známky diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 7226ff2df..f578ff11f 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -13,6 +13,7 @@ Українська Deutsch Čeština + Slovenčina 15 Minuten diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0cf67fa46..391fdd21f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -46,6 +46,7 @@ Token ist nicht mehr gültig Ungültige email Den zugewiesenen Login anstelle von email verwenden + Benutze den zugewiesenen Login oder E-Mail in @%1$s Ungültige symbol Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers Dieses Datenfeld ist erforderlich @@ -175,7 +176,7 @@ Abwesenheitsgrund (optional) Senden - Abwesenheit erfolgreich entschuldigt! + Absence excuse request sent successfully! Sie müssen mindestens eine Abwesenheit auswählen! Verzeihung @@ -273,6 +274,10 @@ Keine Information über die Glücksnummer. Glücksnummer für heute Die heutige Glücksnummer ist: %d + Verlauf anzeigen + + Glücksnummerverlauf + Keine Information über die Glücksnummer Mobile Geräte Keine Geräte @@ -364,6 +369,7 @@ Nick Nick hinzufügen + Avatar-Farbe wählen Logs teilen Aktualisieren @@ -398,21 +404,21 @@ Dunkel Systemthema - Erscheinungsbild + Aussehen & Verhalten Standard Ansicht Berechnung des Jahresenddurchschnitts Mittelwertberechnung durch App erzwingen - Anwesenheit in Schulbesuch zeigen - Thema der Anwendung + Anwesendheit zeigen + Thema Noten erweitern - Aktuelle Lektion im Stundenplan markieren - Gruppen neben Schulfächer im Zeitplan anzeigen + Aktuelle Lektion markieren + Gruppen neben Schulfächen anzeigen Liste der Diagramme in Klassenbewertungen anzeigen Unterricht der ganzen Klasse anzeigen - Schulfächer ohne Noten in \"Noten\" anzeigen + Schulfächer ohne Noten anzeigen Farbschema der Noten - Schulfächer sortieren in \"Noten\" - App Sprache + Schulfachen sortieren + Sprache Benachrichtigungen Benachrichtigungen anzeigen Benachrichtigungen über bevorstehende Lektionen anzeigen @@ -420,6 +426,7 @@ 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 ist deaktiviert Synchronisierung Automatische Aktualisierung An Feiertagen suspendiert @@ -429,14 +436,27 @@ 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 + Erweitert + Aussehen & Verhalten + Benachrichtigungen + Synchronisierung + Noten + Schulbesuch + Zeitplan + Noten + Nachrichten + Aussehen & Verhalten + Sprachen, Themen, Schulfachen sortieren + App-Benachrichtigungen, Probleme beheben + Benachrichtigungen + Synchronisierung + Automatisches Update, Synchronisierungsintervall + Plus und Minus Werte, Durchschnittsberechnung + Erweitert + App-Version, Mitarbeiter, soziale Portale, Lizenzen Neue Einträge im Klassenbuch Neue Noten diff --git a/app/src/main/res/values-lt-v29/preferences_values.xml b/app/src/main/res/values-lt-v29/preferences_values.xml new file mode 100644 index 000000000..18cbd4cf5 --- /dev/null +++ b/app/src/main/res/values-lt-v29/preferences_values.xml @@ -0,0 +1,9 @@ + + + + Sistemos tema + Šviesi + Tamsi + Juoda (AMOLED) + + diff --git a/app/src/main/res/values-lt/preferences_values.xml b/app/src/main/res/values-lt/preferences_values.xml new file mode 100644 index 000000000..fcb637d23 --- /dev/null +++ b/app/src/main/res/values-lt/preferences_values.xml @@ -0,0 +1,53 @@ + + + + Šviesi + Tamsi + Juoda (AMOLED) + + + Sistemos kalba + Polski + English + Pусский + Українська + Deutsch + Čeština + Slovenčina + + + 15 minučių + 30 minučių + 1 valandą + 2 valandas + 6 valandas + 12 valandas + 24 valandas + + + 0,00 + 0,25 + 0,33 + 0,5 + 0,75 + + + Pagal abėcėlę + Pagal datą + + + Dzienniczek+ + Wulkanowy + Laipsnio spalvos registre + + + Antrojo semestro laipsnių vidurkis + Abiejų semestrų laipsnių vidurkis + Visų metų laipsnių vidurkis + + + Nerodyti + Rodyti viską + Rodyti mažesnį + + diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml new file mode 100644 index 000000000..569df4bc8 --- /dev/null +++ b/app/src/main/res/values-lt/strings.xml @@ -0,0 +1,535 @@ + + + + Login + Wulkanowy + Laipsnis + Attendance + Exams + Timetable + Nustatymai + Daugiau + Apie + Peržiūrėti žurnalą + Prisidėję + Licencijos + Žinutės + Nauja žinutė + Pastabos ir pasiekimai + Namų darbai + Paskyros valdymas + Pasirinkite paskyrą + Paskyros informacija + Studentų informacija + + Semestras %1$d, %2$d/%3$d + + Sign in with the student or parent account + Enter the symbol from the register page + Vartotojo vardas + El. paštas + Prisijunkite, PESEL arba el. paštas + Slaptažodis + UONET+ register variant + Mobile API + Scraper + Hibridas + Token + PIN + API raktas + Symbol + Prisijungti + Slaptažodis per trumpas + Login details are incorrect. Make sure the correct UONET+ register variation is selected in the field below + Netinkamas PIN kodas + Invalid token + Token expired + Neteisingas el. paštas + Use the assigned login instead of email + Use the assigned login or email in @%1$s + Netinkamas simbolis + Student not found. Validate the symbol and the chosen variation of the UONET+ register + 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.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the previous screen. Wulkanowy does not detect pre-school students at the moment + Select students to log in to the application + Papildomi nustatymai + 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 + This mode displays the same data as it appears on the register website + The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase + Privatumo politika + Trouble signing in? Contact us! + El. paštas + Discord + Send email + Describe details of problem: + Make sure you select the correct UONET+ register variation! + I forgot my password + Recover your account + Atkurti + Student is already signed in + + Paskyros valdymas + Prisijungti + Sesijos laikas baigėsi + Sesijos laikas baigėsi, prašome prisijungti iš naujo + + Laipsnis + Semestras %d + Keisti semestrą + Jokių laipsnių + Svoris + Svoris: %s + Pastabos + Jokių naujų laipsnių + Number of new ratings: %1$d + Vidurkis: %1$.2f + Puantai: %s + Jokiu vidurkis + Numatomas: %1$s + Galutinis: %1$s + Bendras punktų skaičius + Galutinis laipsnis + Numatomas laipsnis + Calculated average + Final average + Summary + Class + Mark as read + Partial + Semester + Points + Legend + Average: %1$s + Class + Student + + %d grade + %d grades + %d grades + %d grades + + + New grade + New grades + New grades + New grades + + + New predicted grade + New predicted grades + New predicted grades + New predicted grades + + + New final grade + New final grades + New final grades + New final grades + + + You received %1$d grade + You received %1$d grades + You received %1$d grades + You received %1$d grades + + + 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 + + + Lesson + Room + Group + Hours + Changes + No lessons this day + %s min + %s sec + %1$s left + in %1$s + Finished + Now: %s + Next: %s + Later: %s + + Completed lessons + Show completed lessons + No info about completed lessons + Topic + Absence + Resources + + Additional lessons + Show additional lessons + No info about additional lessons + + Attendance summary + Absent for school reasons + Excused absence + Unexcused absence + Exemption + Excused lateness + Unexcused lateness + Present + Deleted + Unknown + Number of lesson + No entries + + %1$d absence + %1$d absences + %1$d absences + %1$d absences + + Absence reason (optional) + Send + Absence excuse request sent successfully! + You must select at least one absence! + Excuse + + Attendance + Total + + No exams this week + Type + Entry date + + Inbox + Sent + Trash + (no subject) + No messages + An error occurred while downloading message content + From: + To: + Date: %s + Reply + Forward + Delete + Move to trash + Delete permanently + Message deleted successfully + Share + Print + Subject + Content + Message sent successfully + Message does not exist + You need to choose at least 1 recipient + The message content must be at least 3 characters + + %d message + %d messages + %d messages + %d messages + + + New message + New messages + New messages + New messages + + + You received %1$d message + You received %1$d messages + You received %1$d messages + You received %1$d messages + + + No info about notes + Points + + %d note + %d notes + %d notes + %d notes + + + New note + New notes + New notes + New notes + + + You received %1$d note + You received %1$d notes + You received %1$d notes + You received %1$d notes + + + + %d praise + %d praises + %d praises + %d praises + + + New praise + New praises + New praises + New praises + + + You received %1$d praise + You received %1$d praises + You received %1$d praises + You received %1$d praises + + + + %d neutral note + %d neutral notes + %d neutral notes + %d neutral notes + + + New neutral note + New neutral notes + New neutral notes + New neutral notes + + + You received %1$d neutral note + You received %1$d neutral notes + You received %1$d neutral notes + You received %1$d neutral notes + + + No info about homework + Mark as done + Mark as undone + Attachments + + Lucky number + Today\'s lucky number is + No info about the lucky number + Lucky number for today + Today\'s lucky number is: %d + Show history + + Lucky number history + No info about lucky numbers + + Mobile devices + No devices + Deregister + Device removed + QR code + Token + Symbol + PIN + + School and teachers + + School + No info about school + School name + School address + Telephone + Name of headmaster + Name of pedagogue + Show on map + Call + + Teachers + No info about teachers + No subject + + Conferences + No info about conferences + + Add account + Logout + Do you want to log out this student? + Student logout + Student account + Parent account + Mobile API mode + Hybrid mode + Edit data + Accounts manager + Select student + Family + Contact + Residence details + Personal information + + App version + Contributors + List of Wulkanowy developers + Report a bug + Send a bug report via e-mail + FAQ + Read Frequently Asked Questions + Discord server + Join the Wulkanowy community + Facebook fanpage + Like our facebook fanpage + Privacy policy + Rules for collecting personal data + Homepage + Visit the website and help develop the application + Licenses + Licenses of libraries used in the application + + License + + Avatar + See more on GitHub + + No info about student or student family + Name + Second name + Gender + Polish citizenship + Family name + Mother\'s and father\'s names + Phone + Cellphone + E-mail + Address of residence + Address of registration + Correspondence address + Surname and first name + Degree of kinship + Address + Phones + Male + Female + Last name + + Nick + Add nick + Choose avatar color + + Share logs + Refresh + + Check for updates + Before reporting a bug, check first if an update with the bug fix is available + + Content + Retry + Description + No description + Teacher + Date + Entry date + Color + Details + Category + Close + No data + Subject + Prev + Next + Search + Search… + Yes + No + Save + + No lessons + Choose theme + Light + Dark + System Theme + + App appearance & behavior + Default view + Calculation of the end-of-year average + Force average calculation by app + Show presence + Theme + Expand grades + Mark current lesson + Show groups next to subjects + Show chart list in class grades + Show whole class lessons + Show subjects without grades + Grades color scheme + Subjects sorting + 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 is disabled + Synchronization + Automatic update + Suspended on holidays + Updates interval + Wi-Fi only + Sync now + Synced! + Sync failed + Sync in progress + Value of the plus + Value of the minus + Reply with message history + Advanced + Appearance & Behavior + Notifications + Synchronization + Grades + Attendance + Timetable + Grades + Messages + Appearance & Behavior + Languages, themes, subjects sorting + App notifications, fix problems + Notifications + Synchronization + Automatic update, synchronization interval + Plus and minus values, average calculation + Advanced + App version, contributors, social portals, licenses + + New entries in register + New grades + Lucky number + New messages + New notes + Push notifications + Upcoming lessons + Debug + + Black + Red + Blue + Green + Purple + No color + + Copied + Undo + + Download of updates has started… + An update has just been downloaded. + Restart + Update failed! Wulkanowy may not function properly. Consider updating + + No internet connection + Connection to register failed. Servers can be overloaded. Please try again later + Loading data failed. Please try again later + Register password change required + Maintenance underway UONET + register. Try again later + Unknown UONET + register error. Try again later + Unknown application error. Please try again later + An unexpected error occurred + Feature disabled by your school + Feature not available. Login in a mode other than Mobile API + diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index bf5cd7697..b218fbc87 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -10,14 +10,43 @@ @color/colorSwipeRefreshDark ?colorSurface ?android:textColorPrimary - @android:color/black + + @color/colorNavigationBarLight + + + @color/colorStatusBarLight - @android:color/black false true + false + + + + + + + + diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 0634f6572..b25dc3e59 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -13,6 +13,7 @@ Українська Deutsch Čeština + Slovenčina 15 minut @@ -24,7 +25,7 @@ 24 godziny - 0,0 + 0,00 0,25 0,33 0,5 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index f45886b77..0d4395838 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -46,6 +46,7 @@ Token stracił ważność Niepoprawny adres email Użyj przydzielonego loginu zamiast emaila + Użyj przypisanego loginu lub adresu e-mail w @%1$s Niepoprawny symbol Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+ To pole jest wymagane @@ -191,7 +192,7 @@ Powód nieobecności (opcjonalny) Wyślij - Usprawiedliwiono pomyślnie! + Prośba o usprawiedliwienie została pomyślnie wysłana! Musisz wybrać co najmniej jedną nieobecność! Usprawiedliw @@ -313,6 +314,10 @@ Brak informacji o szczęśliwym numerku Szczęśliwy numerek na dzisiaj Dziś szczęśliwym numerkiem jest: %d + Pokaż historię + + Historia numerków + Brak informacji o szczęśliwych numerkach Dostęp mobilny Brak urządzeń @@ -404,6 +409,7 @@ Pseudonim Dodaj pseudonim + Wybierz kolor awatara Udostępnij logi Odśwież @@ -438,21 +444,21 @@ Ciemny Motyw systemu - Wygląd + Wygląd i zachowanie aplikacji Domyślny widok Obliczanie średniej końcoworocznej Wymuś obliczanie średniej przez aplikację - Pokazuj obecność we frekwencji - Motyw aplikacji + Pokazuj obecność + Motyw Rozwiń oceny - Oznaczaj bieżącą lekcję na planie - Pokazuj grupę obok przedmiotu na planie + Oznaczaj bieżącą lekcję + Pokazuj grupę obok przedmiotu Pokazuj listę wykresów w ocenach klasy Pokazuj lekcje całej klasy - Pokazuj przedmioty bez ocen w Ocenach + Pokazuj przedmioty bez ocen Schemat kolorów ocen - Sortowanie przedmiotów w \"Oceny\" - Język aplikacji + Sortowanie przedmiotów + Język Powiadomienia Pokazuj powiadomienia Pokazuj powiadomienia o nadchodzących lekcjach @@ -460,6 +466,7 @@ 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 jest wyłączona Synchronizacja Automatyczna aktualizacja Zawieszona na wakacjach @@ -469,14 +476,27 @@ Zsynchronizowano! Synchronizacja nie powiodła się Synchronizacja w trakcie - Synchronizacja - 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 + Zaawansowane + Wygląd i zachowanie + Powiadomienia + Synchronizacja + Oceny + Frekwencja + Plan lekcji + Oceny + Wiadomości + Wygląd i zachowanie + Języki, motywy, sortowanie przedmiotów + Powiadomienia aplikacji, naprawianie problemów + Powiadomienia + Synchronizacja + Automatyczna aktualizacja, interwał synchronizacji + Wartości plusa i minusa, obliczanie średniej + Zaawansowane + Wersja aplikacji, twórcy, media społecznościowe, licencje Nowe wpisy w dzienniku Nowe oceny diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 304d678f7..64b777fb3 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -13,6 +13,7 @@ Українська Deutsch Čeština + Slovenčina 15 минут diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0de20ea91..b614ac9e9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -46,6 +46,7 @@ Token просрочен Неверный адрес электронной почты Используйте назначенный логин вместо электронной почты + Использовать назначенный логин или email в @%1$s Неправильный символ Студент не найден. Подтвердите символ и выбранный вариант регистра UONET+ Это обязательное поле @@ -191,7 +192,7 @@ Причина отсутствия (необязательно) Послать - Статус отсутствия изменён + Absence excuse request sent successfully! Выберите хотя-бы одно отсутствие Изменить статус @@ -313,6 +314,10 @@ Нет данных о счастливом номере Сегодняшний счастливый номер Сегодняшний счастливый номер это: %d + Показать историю + + История удачных чисел + Нет информации о номерах удачи Мобильные устройства Нет устройств @@ -404,6 +409,7 @@ Ник Добавить ник + Выберите цвет аватара Поделиться логами Обновить @@ -438,21 +444,21 @@ Тёмная Тема системы - Вид + Внешний вид приложения & поведение Окно по умолчанию Способ определения средней годовой оценки Принудительно высчитать среднюю оценку через приложение - Показывать присутствия в посещаемости - Тема приложения + Показать присутствие + Тема Разворачивать оценки - Отмечать текущий урок в расписании - Показать группу возле предмета в расписании + Отметить текущий урок + Показать группы рядом с темами Показывать диаграммы в оценках класса Показать уроки всего класса - Показывать предметы без оценок в \"Оценках\" + Показать предметы без оценок Схема цветов оценок - Сортировка предметов в \"Оценках\" - Язык приложения + Сортировка уроков + Язык Уведомления Показывать уведомления Показывать уведомления о будущих уроках @@ -460,6 +466,7 @@ На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства. Перейти в настройски Показывать дебаг-уведомления + Синхронизация отключена Синхронизация Автоматическая синхронизация Приостановить синхронизации во время каникул @@ -469,14 +476,27 @@ Синхронизировано! Синхронизация не удалась Идёт синхронизация - Синхронизация - Ручная синхронизация не обновляет данные в приложении. - \nЧтобы увидеть обновлённые данные, перезапустите приложение. - - Другие Стоимость плюса Стоимость минуса Отвечать с историей сообщений + Расширенные + Внешний вид & Поведение + Уведомления + Синхронизация + Оценки + Посещаемость + Расписание + Оценки + Сообщения + Внешний вид & Поведение + Языки, темы, темы сортировки темы + Уведомления приложений, проблемы с устранением + Уведомления + Синхронизация + Автоматическое обновление, интервал синхронизации + Значения плюс и минус, средний расчет + Расширенные + Версия приложения, участники, социальные порталы, лицензии Новые данные в дневнике Новые оценки diff --git a/app/src/main/res/values-sk-v29/preferences_values.xml b/app/src/main/res/values-sk-v29/preferences_values.xml new file mode 100644 index 000000000..8c5306bb7 --- /dev/null +++ b/app/src/main/res/values-sk-v29/preferences_values.xml @@ -0,0 +1,9 @@ + + + + Motív systému + Svetlý + Tmavý + Čierny (AMOLED) + + \ No newline at end of file diff --git a/app/src/main/res/values-sk-rSK/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml similarity index 98% rename from app/src/main/res/values-sk-rSK/preferences_values.xml rename to app/src/main/res/values-sk/preferences_values.xml index 3275166dc..b0ba9839c 100644 --- a/app/src/main/res/values-sk-rSK/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -13,6 +13,7 @@ Українська Deutsch Čeština + Slovenčina 15 minút diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk/strings.xml similarity index 92% rename from app/src/main/res/values-sk-rSK/strings.xml rename to app/src/main/res/values-sk/strings.xml index f0b8591af..601bd880e 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -46,6 +46,7 @@ Platnosť tokenu vypršala Neplatný e-mail Namiesto e-mailu použite priradené prihlasovacie údaje + Použite priradené prihlasovacie alebo e-mail v @%1$s Neplatný symbol Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ Toto pole je povinné @@ -191,7 +192,7 @@ Dôvod neprítomnosti (voliteľný) Poslať - Neprítomnosť úspešne ospravedlnená! + Žiadosť o ospravedlnenie neprítomnosti bola úspešne odoslaná! Musíte vybrať aspoň jednu neprítomnosť! Ospravedlniť @@ -313,6 +314,10 @@ Žiadne informácie o šťastnom čísle Šťastné číslo pre dnešok Dnes je šťastným číslom: %d + Zobraziť históriu + + História šťastných čísel + Žiadne informácie o šťastných číslach Mobilný prístup Žiadne zariadenia @@ -404,6 +409,7 @@ Prezývka Pridať prezývku + Vybrať farbu avataru Zdieľať protokoly Obnoviť @@ -438,21 +444,21 @@ Tmavý Motív systému - Vzhľad + Vzhľad a správanie aplikácií Predvolené zobrazenie Výpočet koncoročního priemeru Vynútiť priemerný výpočet podľa aplikácie - Zobraziť prítomnosť v dochádzke - Motív aplikácie + Zobraziť prítomnosť + Motív Rozbaliť známky - Označiť aktuálne lekciu v pláne lekcie - Zobraziť skupiny vedľa predmetov v pláne lekcie + Označiť aktuálne lekciu + Zobraziť skupiny vedľa predmetov Zobraziť zoznam grafov v známkach triedy Zobraziť lekcie pre celú triedu - Zobraziť predmety bez známok v \"Známky\" + Zobraziť predmety bez známok Známky farebnú schému - Triedenie predmetov v \"Známky\" - Jazyk aplikácie + Triedenie predmetov + Jazyk Upozornenia Zobraziť upozornenia Zobraziť upozornenia o nadchádzajúcej lekciu @@ -460,6 +466,7 @@ Vaše zariadenie môže mať problémy so synchronizáciou dát as upozorneniami.\n\nAk ich chcete opraviť, pridajte Wulkanového do funkcie Autostart a vypnite optimalizáciu/úsporu batérie v nastavení systému telefóne. Prejsť do nastavení Zobraziť upozornenia o ladení + Synchronizácia je vypnutá Synchronizácia Automatická aktualizácia Pozastavený počas dovolenky @@ -469,14 +476,27 @@ Synchronizovano! Synchronizácia zlyhala Prebieha synchronizácia - Synchronizácia - Ručná synchronizácia neobnoví zobrazenie aplikácie. - \nAk chcete zobraziť synchronizované údaje, reštartujte aplikáciu po synchronizácii. - - Iné Hodnota plusu Hodnota mínusu Odpovedať s históriou správ + Pokročilé + Vzhľad a správanie + Upozornenia + Synchronizácia + Známky + Dochádzka + Plán lekcie + Známky + Správy + Vzhľad a správanie + Jazyky, motívy, triedenie predmetov + Upozornenia aplikácie, oprava problémov + Upozornenia + Synchronizácia + Automatická aktualizácia, interval aktualizácií + Hodnota plusu a mínusu, výpočet priemeru + Pokročilé + Verzia aplikácie, prispievatelia, sociálne portály, licencie Nové položky v denníku Nové známky diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index 02b6ccf85..d0891925d 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -13,6 +13,7 @@ Українська Deutsch Čeština + Slovenčina 15 хвилин diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 843c36161..61a6bb3f2 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -46,6 +46,7 @@ Минув термін дії токену Недійсна адреса електронної пошти Використовуйте призначений логін замість електронної пошти + Використовуйте призначений логін або електронну адресу в @% 1 $ s Неправильний симбвол Студента не знайдено Перевірте символ та обраний варіант реєстру UONET+ Обов\'язкове поле @@ -191,7 +192,7 @@ Причина відсутності (необов’язково) Надіслати - Змінено статус відсутності + Absence excuse request sent successfully! Оберіть хоча б одну відсутність Змінити статус @@ -313,6 +314,10 @@ Брак інформації о щасливому номері Сьогоднішній щасливий номер Сьогоднішнім щасливим номером є %d + Показати історію + + Історія щасливих чисел + Немає інформації про щасливі номери Мобільні пристрої Брак пристроїв @@ -404,6 +409,7 @@ Псевдонім Додати псевдонім + Оберіть колір аватара Поділитися логами Оновити @@ -438,21 +444,21 @@ Темна Тема системи - Вигляд + Поява додатка & amp; поведінки Вікно за замовчуванням Спосіб облічування оцінки на кінець року Примусово розрахувати середню оцінку через додаток - Показувати присутність у відвідуваності - Тема додатку + Показати присутність + Тема Більше оцінок - Позначити поточний урок у розкладі - Покажіть групи поруч із предметами в розкладі + Позначити поточний урок + Показувати групи поруч з темами Показувати діаграми в оцінках класу Показати уроки всього класу - Показуйте предмети без оцінок у оцінках + Показати предмети без оцінок Схема кольорів оцінок - Сортування предметів за \"Оцінками\" - Мова додатку + Сортування предметів + Мова Повідомлення Показувати повідомлення Показувати повідомлення о наступних уроках @@ -460,6 +466,7 @@ На вашому пристрої можуть бути помилки з синхронізацією і повідомленнями\n\nЩоб виправити іх, вам необхідно додати Wulkanowy в авто-старт и вимкнути оптимізацію/экономію батареї в налаштуваннях пристрою. Перейти до налаштувань Показувати дебаг-повідомлення + Синхронізація вимкнена Синхронізація Автоматична синхронізація Призупинено на час канікул @@ -469,14 +476,27 @@ Синхронізовано! Синхронізація не вдалася Триває синхронізація - Синхронізація - Ручна синхронізація не оновлює дані в додатку. - \nЩоб побачити оновлені дані, перезавантажте додаток. - - Інші Вартість плюсу Вага мінуса Відповісти з історією повідомлень + Додатково + Вигляд & Поведінка + Повідомлення + Синхронізація + Оцінки + Відвідуваність + Розклад + Класи + Повідомлення + Вигляд & Поведінка + Мови, теми, тема сортування + Сповіщення додатку, виправляти проблеми + Повідомлення + Синхронізація + Автоматичне оновлення, інтервал синхронізації + Плюс і мінус значення, середні обчислення + Додатково + Версія програми, учасники, соціальні портали, ліцензії Нові дані в щоденнику Нові оцінки diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml new file mode 100644 index 000000000..574e8488d --- /dev/null +++ b/app/src/main/res/values-v23/styles.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v26/styles.xml b/app/src/main/res/values-v26/styles.xml new file mode 100644 index 000000000..55413c053 --- /dev/null +++ b/app/src/main/res/values-v26/styles.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v28/styles.xml similarity index 75% rename from app/src/main/res/values-v27/styles.xml rename to app/src/main/res/values-v28/styles.xml index d33f64228..ee77091d3 100644 --- a/app/src/main/res/values-v27/styles.xml +++ b/app/src/main/res/values-v28/styles.xml @@ -1,9 +1,10 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml new file mode 100644 index 000000000..ee77091d3 --- /dev/null +++ b/app/src/main/res/values-v29/styles.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 0a43612d6..158490471 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -14,7 +14,7 @@ Skarżysko-Kamienna - e-Skarżysko Łask - Platforma vEdukacja Powiat łaski - Platforma edukacyjna - Powiat Krasnostawski - Platforma oświatowa + Powiat krasnostawski - Platforma oświatowa Powiat kętrzyński - Platforma e-Usług Gmina Ulan-Majorat - Platforma oświatowa Gmina Ozorków - Platforma edukacyjna diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 994567444..b2e0dcef1 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,6 +6,12 @@ #ff5722 #e84853 + #2D2D2D + #1E1E1E + + #1C1C1C + #0D0D0D + #ffd54f #ff8f00 diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 5f2aec4ce..44f54c170 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -32,6 +32,7 @@ Українська Deutsch Čeština + Slovenčina system @@ -41,6 +42,7 @@ uk de cs + sk diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c21146c95..db5ed7b49 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Token expired Invalid email Use the assigned login instead of email + Use the assigned login or email in @%1$s Invalid symbol Student not found. Validate the symbol and the chosen variation of the UONET+ register This field is required @@ -193,7 +194,7 @@ Absence reason (optional) Send - Absence excused successfully! + Absence excuse request sent successfully! You must select at least one absence! Excuse @@ -305,6 +306,11 @@ No info about the lucky number Lucky number for today Today\'s lucky number is: %d + Show history + + + Lucky number history + No info about lucky numbers Mobile devices @@ -418,6 +424,7 @@ Nick Add nick + Choose avatar color @@ -462,21 +469,21 @@ - Appearance + App appearance & behavior Default view Calculation of the end-of-year average Force average calculation by app - Show presence in attendance - Application theme + Show presence + Theme Expand grades - Mark current lesson in timetable - Show groups next to subjects in timetable + Mark current lesson + Show groups next to subjects Show chart list in class grades Show whole class lessons - Show subjects without grades in Grades + Show subjects without grades Grades color scheme - Subjects sorting in "Grades" - App language + Subjects sorting + Language Notifications Show notifications @@ -485,6 +492,7 @@ 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 is disabled Synchronization Automatic update @@ -495,17 +503,32 @@ Synced! Sync failed Sync in progress - Synchronization - - Manual sync doesn\'t refresh app views. - \nTo see the synced data relaunch the app after syncing. - - Other Value of the plus Value of the minus Reply with message history + Advanced + Appearance & Behavior + Notifications + Synchronization + + Grades + Attendance + Timetable + Grades + Messages + + Appearance & Behavior + Languages, themes, subjects sorting + App notifications, fix problems + Notifications + Synchronization + Automatic update, synchronization interval + Plus and minus values, average calculation + Advanced + App version, contributors, social portals, licenses + New entries in register @@ -549,5 +572,4 @@ An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index cf587cbf8..fc7ffb77f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -13,6 +13,7 @@ @color/colorSwipeRefresh ?android:textColorPrimary @style/PreferenceThemeOverlay + false