diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index 491de21b..c43a4972 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -183,7 +183,7 @@ jobs: run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT - name: Upload workflow artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: ${{ steps.changelog.outputs.appVersionName }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 7a72d8eb..46968c3d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -243,6 +243,14 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { loggingManager.cleanupHyperLogDatabase() notificationManager.registerAllChannels() shortcutManager.createShortcuts() + + if (config.appVersionCore < BuildConfig.VERSION_CODE) { + // force syncing all endpoints on update + db.endpointTimerDao().clear() + config.sync.lastAppSync = 0L + config.hash = "invalid" + config.appVersionCore = BuildConfig.VERSION_CODE + } SSLProviderInstaller.install(applicationContext, this@App::buildHttp) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/core/manager/GradesManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/core/manager/GradesManager.kt index ca8f01a5..e2864ecf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/core/manager/GradesManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/core/manager/GradesManager.kt @@ -13,9 +13,11 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_AVG import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_SUM import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_FINAL +import pl.szczodrzynski.edziennik.data.db.enums.SchoolType import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.ext.asColoredSpannable import pl.szczodrzynski.edziennik.ext.get @@ -43,6 +45,8 @@ class GradesManager(val app: App) : CoroutineScope { const val YEAR_ALL_GRADES = 4 const val COLOR_MODE_DEFAULT = 0 const val COLOR_MODE_WEIGHTED = 1 + const val UNIVERSITY_AVERAGE_MODE_SIMPLE = 0 + const val UNIVERSITY_AVERAGE_MODE_ECTS = 1 } private val job = Job() @@ -70,6 +74,8 @@ class GradesManager(val app: App) : CoroutineScope { get() = app.profile.config.grades.hideImproved val averageWithoutWeight get() = app.profile.config.grades.averageWithoutWeight + val isUniversity + get() = app.profile.loginStoreType.schoolType == SchoolType.UNIVERSITY fun getOrderByString() = when (orderBy) { @@ -88,6 +94,7 @@ class GradesManager(val app: App) : CoroutineScope { else context.getString(R.string.grades_weight_format, format.format(grade.weight)) TYPE_POINT_AVG -> context.getString(R.string.grades_max_points_format, format.format(grade.valueMax)) + TYPE_NO_GRADE -> context.getString(R.string.grades_weight_no_grade) else -> null } @@ -159,6 +166,19 @@ class GradesManager(val app: App) : CoroutineScope { } } type == TYPE_NORMAL && defColor -> grade.color and 0xffffff + type == TYPE_NORMAL && app.profile.loginStoreType.schoolType == SchoolType.UNIVERSITY -> { + when (grade.name.lowercase()) { + "zal" -> 0x4caf50 + "nb", "nk" -> 0xff7043 + "2.0", "nzal" -> 0xff3d00 + "3.0" -> 0xffff00 + "3.5" -> 0xc6ff00 + "4.0" -> 0x76ff03 + "4.5" -> 0x64dd17 + "5.0" -> 0x00c853 + else -> grade.color and 0xffffff + } + } type in TYPE_NORMAL..TYPE_YEAR_FINAL -> { when (grade.name.lowercase()) { "+", "++", "+++" -> 0x4caf50 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt index ad52ba1c..b0a39c95 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt @@ -114,6 +114,11 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa } } + if (profile?.empty == true) { + // force app sync on first login + app.config.sync.lastAppSync = 0L + } + edziennikInterface = when (loginStore.type) { LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback) LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt index 90008356..2cd4b305 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -4,6 +4,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos +import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.db.entity.LoginStore @@ -73,4 +74,9 @@ class DataUsos( get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } set(value) { profile["studentId"] = value; mStudentId = value } private var mStudentId: Int? = null + + var termNames: Map<String, String> = mapOf() + get() { mTermNames = mTermNames ?: profile?.getStudentData("termNames", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mTermNames ?: mapOf() } + set(value) { profile["termNames"] = app.gson.toJson(value); mTermNames = value } + private var mTermNames: Map<String, String>? = null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt index ccd5aae8..299bebcc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt @@ -10,10 +10,12 @@ import pl.szczodrzynski.edziennik.data.enums.FeatureType import pl.szczodrzynski.edziennik.data.enums.LoginMethod import pl.szczodrzynski.edziennik.data.enums.LoginType -const val ENDPOINT_USOS_API_USER = 7000 -const val ENDPOINT_USOS_API_TERMS = 7010 -const val ENDPOINT_USOS_API_COURSES = 7020 -const val ENDPOINT_USOS_API_TIMETABLE = 7030 +const val ENDPOINT_USOS_API_USER = 7000 +const val ENDPOINT_USOS_API_TERMS = 7010 +const val ENDPOINT_USOS_API_COURSES = 7020 +const val ENDPOINT_USOS_API_TIMETABLE = 7030 +const val ENDPOINT_USOS_API_ECTS_POINTS = 7040 +const val ENDPOINT_USOS_API_EXAM_REPORTS = 7050 val UsosFeatures = listOf( /* @@ -39,4 +41,12 @@ val UsosFeatures = listOf( Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf( ENDPOINT_USOS_API_TIMETABLE to LoginMethod.USOS_API, )), + + /* + * Grades + */ + Feature(LoginType.USOS, FeatureType.GRADES, listOf( + ENDPOINT_USOS_API_ECTS_POINTS to LoginMethod.USOS_API, + ENDPOINT_USOS_API_EXAM_REPORTS to LoginMethod.USOS_API, + )), ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt index 240907cb..93e4aa62 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -7,6 +7,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.edziennik.usos.* import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiEctsPoints +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiExamReports import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser @@ -57,6 +59,14 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_endpoint_timetable) UsosApiTimetable(data, lastSync, onSuccess) } + ENDPOINT_USOS_API_ECTS_POINTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_categories) + UsosApiEctsPoints(data, lastSync, onSuccess) + } + ENDPOINT_USOS_API_EXAM_REPORTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + UsosApiExamReports(data, lastSync, onSuccess) + } else -> onSuccess(endpointId) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt index e93c63dd..e5086b15 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt @@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.ext.* @@ -25,17 +26,20 @@ class UsosApiCourses( apiRequest<JsonObject>( tag = TAG, service = "courses/user", + params = mapOf( + "active_terms_only" to false, + ), fields = listOf( // "terms" to listOf("id", "name", "start_date", "end_date"), "course_editions" to listOf( "course_id", "course_name", - // "term_id", "user_groups" to listOf( "course_unit_id", "group_number", - // "class_type", + "class_type", "class_type_id", + "term_id", "lecturers", ), ), @@ -63,22 +67,38 @@ class UsosApiCourses( for (courseEdition in courseEditions) { val courseId = courseEdition.getString("course_id") ?: continue val courseName = courseEdition.getLangString("course_name") ?: continue - val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue + val userGroups = + courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue for (userGroup in userGroups) { val courseUnitId = userGroup.getLong("course_unit_id") ?: continue val groupNumber = userGroup.getInt("group_number") ?: continue - // val classType = userGroup.getLangString("class_type") ?: continue + val classType = userGroup.getLangString("class_type") ?: continue val classTypeId = userGroup.getString("class_type_id") ?: continue + val termId = userGroup.getString("term_id") ?: continue val lecturers = userGroup.getLecturerIds("lecturers") - data.teamList.put(courseUnitId, Team( - profileId, - courseUnitId, - "${profile?.studentClassName} $classTypeId$groupNumber - $courseName", - 2, - "${data.schoolId}:${courseId} $classTypeId$groupNumber", - lecturers.firstOrNull() ?: -1L, - )) + data.teamList.put( + courseUnitId, Team( + profileId, + courseUnitId, + "${profile?.studentClassName} $courseName ($classTypeId$groupNumber)", + 2, + "${data.schoolId}:${termId}:${courseId} $classTypeId$groupNumber", + lecturers.firstOrNull() ?: -1L, + ) + ) + + val gradeCategory = data.gradeCategories[courseUnitId] + data.gradeCategories.put( + courseUnitId, GradeCategory( + profileId, + courseUnitId, + gradeCategory?.weight ?: -1.0f, + 0, + courseId, + ).addColumn(classType) + ) + hasValidTeam = true } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiEctsPoints.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiEctsPoints.kt new file mode 100644 index 00000000..b6d112f1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiEctsPoints.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2025-1-31. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_ECTS_POINTS +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.filter + +class UsosApiEctsPoints( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiEctsPoints" + } + + init { + apiRequest<JsonObject>( + tag = TAG, + service = "courses/user_ects_points", + responseType = ResponseType.OBJECT, + ) { json, response -> + if (!processResponse(json)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.setSyncNext(ENDPOINT_USOS_API_ECTS_POINTS, 2 * DAY) + onSuccess(ENDPOINT_USOS_API_ECTS_POINTS) + } + } + + private fun processResponse(json: JsonObject): Boolean { + for ((_, coursePointsEl) in json.entrySet()) { + if (!coursePointsEl.isJsonObject) + continue + for ((courseId, pointsEl) in coursePointsEl.asJsonObject.entrySet()) { + if (!pointsEl.isJsonPrimitive) + continue + val gradeCategories = data.gradeCategories + .filter { it.text == courseId } + gradeCategories.forEach { + it.weight = pointsEl.asString.toFloatOrNull() ?: -1.0f + } + } + } + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiExamReports.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiExamReports.kt new file mode 100644 index 00000000..f1e44b16 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiExamReports.kt @@ -0,0 +1,212 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2025-1-31. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_EXAM_REPORTS +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.entity.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getBoolean +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getJsonArray +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.join +import pl.szczodrzynski.edziennik.utils.models.Date + +class UsosApiExamReports( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiExamReports" + } + + private val missingTermNames = mutableSetOf<String>() + + init { + apiRequest<JsonObject>( + tag = TAG, + service = "examrep/user2", + fields = listOf( + "id", + "type_description", + "course_unit" to listOf("id", "course_name"), + "sessions" to listOf( + "number", + "description", + "issuer_grades" to listOf( + "value_symbol", + // "value_description", + "passes", + "counts_into_average", + "date_modified", + "modification_author", + "comment", + ), + ), + ), + responseType = ResponseType.OBJECT, + ) { json, response -> + if (!processResponse(json)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.toRemove.add(DataRemoveModel.Grades.all()) + data.setSyncNext(ENDPOINT_USOS_API_EXAM_REPORTS, SYNC_ALWAYS) + + if (missingTermNames.isEmpty()) + onSuccess(ENDPOINT_USOS_API_EXAM_REPORTS) + else + UsosApiTerms(data, lastSync, onSuccess, missingTermNames) + } + } + + private fun processResponse(json: JsonObject): Boolean { + for ((termId, courseEditionEl) in json.entrySet()) { + if (!courseEditionEl.isJsonObject) + continue + for ((courseId, examReportsEl) in courseEditionEl.asJsonObject.entrySet()) { + if (!examReportsEl.isJsonArray) + continue + for (examReportEl in examReportsEl.asJsonArray) { + if (!examReportEl.isJsonObject) + continue + val examReport = examReportEl.asJsonObject + processExamReport(termId, courseId, examReport) + } + } + } + return true + } + + private fun processExamReport(termId: String, courseId: String, examReport: JsonObject) { + val examId = examReport.getString("id")?.toIntOrNull() + ?: return + val typeDescription = examReport.getLangString("type_description") + val courseUnit = examReport.getJsonObject("course_unit") + ?: return + val courseUnitId = courseUnit.getString("id")?.toLongOrNull() + ?: return + val courseName = courseUnit.getLangString("course_name") + ?: return + val sessions = examReport.getJsonArray("sessions") + ?: return + + val gradeCategory = data.gradeCategories[courseUnitId] + val classType = gradeCategory?.columns?.get(0) + + val subject = data.getSubject( + id = null, + name = courseName, + shortName = courseId, + ) + + var hasGrade = false + + for (sessionEl in sessions) { + if (!sessionEl.isJsonObject) + continue + val session = sessionEl.asJsonObject + + val sessionNumber = session.getInt("number") ?: continue + val sessionDescription = session.getLangString("description") + val issuerGrade = session.getJsonObject("issuer_grades") + + val valueSymbol = issuerGrade.getString("value_symbol") ?: continue + val passes = issuerGrade.getBoolean("passes") + val countsIntoAverage = issuerGrade.getString("counts_into_average") ?: "T" + val dateModified = issuerGrade.getString("date_modified") + val modificationAuthorId = issuerGrade.getJsonObject("modification_author") + ?.getLong("id") ?: -1L + val comment = issuerGrade.getString("comment") + + val value = valueSymbol.toFloatOrNull() ?: 0.0f + + if (termId !in data.termNames) { + missingTermNames.add(termId) + } + + val gradeObject = Grade( + profileId = profileId, + id = examId * 10L + sessionNumber, + name = valueSymbol, + type = TYPE_NORMAL, + value = value, + weight = if (countsIntoAverage == "T") gradeCategory?.weight ?: 0.0f else 0.0f, + color = (if (passes == true) 0xFF465FB3 else 0xFFB71C1C).toInt(), + category = typeDescription, + description = listOfNotNull(classType, sessionDescription, comment).join(" - "), + comment = termId, + semester = 1, + teacherId = modificationAuthorId, + subjectId = subject.id, + addedDate = Date.fromIso(dateModified), + ) + hasGrade = true + + if (sessionNumber > 1) { + val origId = examId * 10L + sessionNumber - 1 + val grades = data.gradeList.filter { it.id == origId } + val improvedGrade = grades.firstOrNull() + improvedGrade?.parentId = gradeObject.id + improvedGrade?.weight = 0.0f + gradeObject.isImprovement = true + } + + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + profileId, + MetadataType.GRADE, + gradeObject.id, + profile?.empty ?: false, + profile?.empty ?: false, + ) + ) + } + + if (!hasGrade) { + // add an "empty" grade for the exam + val gradeObject = Grade( + profileId = profileId, + id = examId * 10L, + name = "...", + type = TYPE_NO_GRADE, + value = 0.0f, + weight = 0.0f, + color = 0xFFBABABD.toInt(), + category = typeDescription, + description = classType, + comment = termId, + semester = 1, + teacherId = -1L, + subjectId = subject.id, + addedDate = 0, + ) + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + profileId, + MetadataType.GRADE, + gradeObject.id, + true, + true, + ) + ) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt index b1b2fd88..93da3aea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api import com.google.gson.JsonArray +import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS @@ -16,43 +17,81 @@ class UsosApiTerms( override val data: DataUsos, override val lastSync: Long?, val onSuccess: (endpointId: Int) -> Unit, + names: Set<String>? = null, ) : UsosApi(data, lastSync) { companion object { const val TAG = "UsosApiTerms" } init { - apiRequest<JsonArray>( - tag = TAG, - service = "terms/search", - params = mapOf( - "query" to Date.getToday().year.toString(), - ), - responseType = ResponseType.ARRAY, - ) { json, response -> - if (!processResponse(json)) { - data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) - return@apiRequest - } + if (names != null) { + apiRequest<JsonObject>( + tag = TAG, + service = "terms/terms", + params = mapOf("term_ids" to names.joinToString("|")), + responseType = ResponseType.OBJECT, + ) { json, response -> + if (!processResponse(json.entrySet().map { it.value.asJsonObject })) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } - data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY) - onSuccess(ENDPOINT_USOS_API_TERMS) + data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY) + onSuccess(ENDPOINT_USOS_API_TERMS) + } + } else { + apiRequest<JsonArray>( + tag = TAG, + service = "terms/search", + params = mapOf("query" to Date.getToday().year.toString()), + responseType = ResponseType.ARRAY, + ) { json, response -> + if (!processResponse(json.asJsonObjectList())) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY) + onSuccess(ENDPOINT_USOS_API_TERMS) + } } } - private fun processResponse(json: JsonArray): Boolean { + private fun processResponse(terms: List<JsonObject>): Boolean { + val profile = profile ?: return false + val termNames = data.termNames.toMutableMap() val today = Date.getToday() - for (term in json.asJsonObjectList()) { + for (term in terms) { + val id = term.getString("id") + val name = term.getLangString("name") + val orderKey = term.getInt("order_key") + if (id != null && name != null) + termNames[id] = "$orderKey$$name" + if (!term.getBoolean("is_active", false)) continue val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } ?: continue val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue - if (today in startDate..finishDate) { - profile?.studentSchoolYearStart = startDate.year - profile?.dateSemester1Start = startDate - profile?.dateSemester2Start = finishDate - } + if (today !in startDate..finishDate) + continue + + if (startDate.month >= 8) + profile.dateSemester1Start = startDate + else + profile.dateSemester2Start = startDate + + if (finishDate.month >= 8) + profile.dateYearEnd = finishDate + else + profile.dateSemester2Start = finishDate } + // update school year start + profile.studentSchoolYearStart = profile.dateSemester1Start.year + // update year end date if there is a new year + if (profile.dateYearEnd <= profile.dateSemester1Start) + profile.dateYearEnd = + profile.dateSemester1Start.clone().setYear(profile.dateSemester1Start.year + 1) + data.termNames = termNames return true } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt index 806c2fd8..7a2a2dbc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt @@ -32,7 +32,8 @@ class UsosApiUser( "last_name", "student_number", "student_programmes" to listOf( - "programme" to listOf("id"), + "id", + "programme" to listOf("id", "description"), ), ), ), @@ -40,9 +41,11 @@ class UsosApiUser( ) { json, response -> val programmes = json.getJsonArray("student_programmes") if (programmes.isNullOrEmpty()) { - data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) - .withApiResponse(json) - .withResponse(response)) + data.error( + ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) + .withApiResponse(json) + .withResponse(response) + ) return@apiRequest } @@ -50,13 +53,19 @@ class UsosApiUser( val lastName = json.getString("last_name") val studentName = buildFullName(firstName, lastName) + val studentProgrammeId = programmes.getJsonObject(0) + .getString("id") + val programmeId = programmes.getJsonObject(0) + .getJsonObject("programme") + .getString("id") + data.studentId = json.getInt("id") ?: data.studentId profile?.studentNameLong = studentName profile?.studentNameShort = studentName.getShortName() profile?.studentNumber = json.getString("student_number")?.replace(Regex("[^0-9]"), "")?.toIntOrNull() ?: -1 - profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id") + profile?.studentClassName = programmeId - profile?.studentClassName?.let { + val team = programmeId?.let { data.getTeam( id = null, name = it, @@ -64,6 +73,7 @@ class UsosApiUser( isTeamClass = true, ) } + team?.code = "${data.schoolId}:${studentProgrammeId}:${programmeId}" data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY) onSuccess(ENDPOINT_USOS_API_USER) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt index dab3e908..3e965061 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -206,7 +206,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { teams.filter { it.profileId == profile.id }.map { it.code } ) val hash = user.toString().md5() - if (hash == profile.config.hash) + if (hash == profile.config.hash && app.config.hash != "invalid") return@mapNotNull null return@mapNotNull user to profile.config } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/config/Config.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/config/Config.kt index bad730a0..4db241f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/config/Config.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/config/Config.kt @@ -60,6 +60,7 @@ class Config(app: App) : BaseConfig<Config>(app, profileId = null) { var appRateSnackbarTime by config<Long>(0L) var lastLogCleanupTime by config<Long>(0L) var appVersion by config<Int>(BuildConfig.VERSION_CODE) + var appVersionCore by config<Int>(0) var validation by config<String?>(null, "buildValidation") var archiverEnabled by config<Boolean>(true) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/config/ProfileConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/config/ProfileConfig.kt index 99183417..4ec342d7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/config/ProfileConfig.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/config/ProfileConfig.kt @@ -15,6 +15,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAUL import pl.szczodrzynski.edziennik.data.enums.NotificationType import pl.szczodrzynski.edziennik.ui.home.HomeCardModel import pl.szczodrzynski.edziennik.core.manager.GradesManager.Companion.COLOR_MODE_WEIGHTED +import pl.szczodrzynski.edziennik.core.manager.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS import pl.szczodrzynski.edziennik.core.manager.GradesManager.Companion.YEAR_ALL_GRADES class ProfileConfig( @@ -46,10 +47,13 @@ class ProfileConfig( var dontCountEnabled by config<Boolean>(false) var dontCountGrades by config<List<String>> { listOf() } var hideImproved by config<Boolean>(false) + var hideNoGrade by base.config<Boolean>(false) var hideSticksFromOld by config<Boolean>(false) var minusValue by config<Float?>(null) var plusValue by config<Float?>(null) var yearAverageMode by config<Int>(YEAR_ALL_GRADES) + var universityAverageMode by config<Int>(UNIVERSITY_AVERAGE_MODE_ECTS) + var countEctsInProgress by config<Boolean>(false) } inner class UI { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EndpointTimerDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EndpointTimerDao.kt index c0ff0464..512218da 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EndpointTimerDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EndpointTimerDao.kt @@ -24,4 +24,7 @@ interface EndpointTimerDao { @Query("DELETE FROM endpointTimers WHERE profileId = :profileId") fun clear(profileId: Int) + + @Query("DELETE FROM endpointTimers") + fun clear() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Grade.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Grade.kt index 5bf3bd86..5e54dc61 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Grade.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Grade.kt @@ -55,6 +55,7 @@ open class Grade( const val TYPE_DESCRIPTIVE = 30 const val TYPE_DESCRIPTIVE_TEXT = 31 const val TYPE_TEXT = 40 + const val TYPE_NO_GRADE = 100 } @ColumnInfo(name = "gradeValueMax") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/enums/LoginTypeFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/enums/LoginTypeFeatures.kt index 68a2a2c1..ed9197c3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/enums/LoginTypeFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/enums/LoginTypeFeatures.kt @@ -93,6 +93,7 @@ internal val FEATURES_PODLASIE = setOf( internal val FEATURES_USOS = setOf( TIMETABLE, AGENDA, + GRADES, STUDENT_INFO, STUDENT_NUMBER, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt index 116afc2d..45df1a0c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt @@ -72,8 +72,7 @@ fun Profile.getAppData() = if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType) fun Profile.shouldArchive(): Boolean { - if (loginStoreType == LoginType.DEMO) - return false + return false // vulcan hotfix if (dateYearEnd.month > 6) { dateYearEnd.month = 6 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt index eb5c3e07..8ec41f93 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt @@ -26,6 +26,17 @@ import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.ext.setOnSelectedListener import pl.szczodrzynski.edziennik.ui.base.dialog.ConfigDialog import pl.szczodrzynski.edziennik.ui.base.dialog.SimpleDialog +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_DEFAULT +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_SUBJECT_ASC +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_SIMPLE +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_AVG +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_SEM +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES class GradesConfigDialog( activity: AppCompatActivity, @@ -38,6 +49,8 @@ class GradesConfigDialog( @SuppressLint("SetTextI18n") override suspend fun loadConfig() { + b.isUniversity = app.gradesManager.isUniversity + b.customPlusCheckBox.isChecked = app.profile.config.grades.plusValue != null b.customPlusValue.isVisible = b.customPlusCheckBox.isChecked b.customMinusCheckBox.isChecked = app.profile.config.grades.minusValue != null @@ -67,10 +80,18 @@ class GradesConfigDialog( else -> null }?.isChecked = true + when (app.profile.config.grades.universityAverageMode) { + UNIVERSITY_AVERAGE_MODE_ECTS -> b.gradeUniversityAverageMode1 + UNIVERSITY_AVERAGE_MODE_SIMPLE -> b.gradeUniversityAverageMode0 + else -> null + }?.isChecked = true + b.dontCountGrades.isChecked = app.profile.config.grades.dontCountEnabled && app.profile.config.grades.dontCountGrades.isNotEmpty() b.hideImproved.isChecked = app.profile.config.grades.hideImproved + b.hideNoGrade.isChecked = app.profile.config.grades.hideNoGrade b.averageWithoutWeight.isChecked = app.profile.config.grades.averageWithoutWeight + b.countEctsInProgress.isChecked = app.profile.config.grades.countEctsInProgress if (app.profile.config.grades.dontCountGrades.isEmpty()) { b.dontCountGradesText.setText("nb, 0, bz, bd") @@ -140,10 +161,21 @@ class GradesConfigDialog( app.profile.config.grades.yearAverageMode = YEAR_1_SEM_2_SEM } + b.gradeUniversityAverageMode1.setOnSelectedListener { + app.profile.config.grades.universityAverageMode = UNIVERSITY_AVERAGE_MODE_ECTS + } + b.gradeUniversityAverageMode0.setOnSelectedListener { + app.profile.config.grades.universityAverageMode = UNIVERSITY_AVERAGE_MODE_SIMPLE + } + b.hideImproved.onChange { _, isChecked -> app.profile.config.grades.hideImproved = isChecked } + b.hideNoGrade.onChange { _, isChecked -> app.profile.config.grades.hideNoGrade = isChecked } b.averageWithoutWeight.onChange { _, isChecked -> app.profile.config.grades.averageWithoutWeight = isChecked } + b.countEctsInProgress.onChange { _, isChecked -> + app.profile.config.grades.countEctsInProgress = isChecked + } b.averageWithoutWeightHelp.onClick { SimpleDialog<Unit>(activity) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt index 05bb1da0..3979903d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.ext.startCoroutineTimer @@ -134,6 +135,7 @@ class GradesAdapter( if (model.state == STATE_CLOSED) { val subItems = when { + model is GradesSubject && manager.isUniversity -> listOf() model is GradesSemester && model.grades.isEmpty() -> listOf(GradesEmpty()) model is GradesSemester && manager.hideImproved -> @@ -147,10 +149,12 @@ class GradesAdapter( if (notifyAdapter) notifyItemInserted(position) } - position++ model.state = STATE_OPENED - items.addAll(position, subItems.filterNotNull()) - if (notifyAdapter) notifyItemRangeInserted(position, subItems.size) + if (subItems.isNotEmpty()) { + position++ + items.addAll(position, subItems.filterNotNull()) + if (notifyAdapter) notifyItemRangeInserted(position, subItems.size) + } if (model is GradesSubject) { // auto expand first semester @@ -232,10 +236,13 @@ class GradesAdapter( } } - if (item !is GradeFull || onGradeClick != null) + if (item !is GradeFull || (onGradeClick != null && item.type != TYPE_NO_GRADE)) { holder.itemView.setOnClickListener(onClickListener) - else + holder.itemView.isEnabled = true + } else { holder.itemView.setOnClickListener(null) + holder.itemView.isEnabled = false + } } fun notifyItemChanged(model: Any) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt index 90173cf8..5ac3da1f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt @@ -17,6 +17,8 @@ import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.core.manager.GradesManager import pl.szczodrzynski.edziennik.data.db.entity.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.data.enums.FeatureType import pl.szczodrzynski.edziennik.data.enums.MetadataType @@ -32,6 +34,10 @@ import pl.szczodrzynski.edziennik.ui.grades.models.GradesAverages import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester import pl.szczodrzynski.edziennik.ui.grades.models.GradesStats import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.utils.TextInputDropDown +import pl.szczodrzynski.edziennik.utils.managers.GradesManager +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_SIMPLE import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import kotlin.math.max @@ -78,6 +84,35 @@ class GradesListFragment : BaseFragment<GradesListFragmentBinding, MainActivity> else -> grades } + if (manager.isUniversity) { + val termIds = grades.map { it.comment }.toSet().toMutableList() + val termNames: MutableMap<String, String> = mutableMapOf() + // deserialize to a map of termId to (orderKey, termName) + val terms = app.profile.getStudentData("termNames", null) + ?.let { app.gson.fromJson(it, termNames::class.java) } + ?.mapValues { (_, value) -> value.split('$', limit = 2) } + ?.mapValues { (_, value) -> Pair(value[0].toIntOrNull() ?: 0, value[1]) } + ?: mapOf() + // sort by order key + termIds.sortByDescending { termId -> terms[termId]?.first ?: 0 } + // populate the dropdown + b.semesterLayout.isVisible = true + b.semesterDropdown.items = termIds.mapIndexed { id, termId -> + TextInputDropDown.Item( + id.toLong(), + terms[termId]?.second ?: termId ?: "-", + tag = termId, + ) + }.toMutableList() + b.semesterDropdown.select(index = 0) + b.semesterDropdown.setOnChangeListener { item -> + b.semesterDropdown.select(item) + adapter.items = processGrades(items) + adapter.notifyDataSetChanged() + return@setOnChangeListener true + } + } + // load & configure the adapter adapter.items = withContext(Dispatchers.Default) { processGrades(items) } if (items.isNotNullNorEmpty() && b.list.adapter == null) { @@ -160,13 +195,23 @@ class GradesListFragment : BaseFragment<GradesListFragmentBinding, MainActivity> var semesterNumber = 0 var subject = GradesSubject(subjectId, "") var semester = GradesSemester(0, 1) + val isUniversity = manager.isUniversity + val filterTermId = b.semesterDropdown.selected?.tag - val hideImproved = manager.hideImproved + val hideNoGrade = app.profile.config.grades.hideNoGrade + val countEctsInProgress = app.profile.config.grades.countEctsInProgress + val universityAverageMode = app.profile.config.grades.universityAverageMode // grades returned by the query are ordered // by the subject ID, so it's easier and probably // a bit faster to build all the models for (grade in grades) { + if (isUniversity && filterTermId != null && grade.comment != filterTermId) + continue + + if (hideNoGrade && grade.type == TYPE_NO_GRADE) + continue + /*if (grade.parentId != null && grade.parentId != -1L) continue // the grade is hidden as a new, improved one is available*/ if (grade.subjectId != subjectId) { @@ -230,8 +275,63 @@ class GradesListFragment : BaseFragment<GradesListFragmentBinding, MainActivity> subject.lastAddedDate = max(subject.lastAddedDate, grade.addedDate) } + when (manager.orderBy) { + GradesManager.ORDER_BY_DATE_DESC -> items.sortByDescending { it.lastAddedDate } + GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate } + } + val stats = GradesStats() + if (isUniversity) { + val semesterSum = mutableListOf<Float>() + val semesterCount = mutableListOf<Float>() + val totalSum = mutableListOf<Float>() + val totalCount = mutableListOf<Float>() + val ectsPoints = mutableMapOf<Pair<Long, String?>, Float>() + for (grade in grades) { + val pointsPair = grade.subjectId to grade.comment + if (grade.type == TYPE_NO_GRADE && !countEctsInProgress) + // reset points if there's an exam that isn't passed yet + ectsPoints[pointsPair] = 0.0f + + if (grade.value == 0.0f || grade.weight == 0.0f) + continue + if (universityAverageMode == UNIVERSITY_AVERAGE_MODE_ECTS) + totalSum.add(grade.value * grade.weight) + else + totalSum.add(grade.value) + totalCount.add(grade.weight) + + if (grade.value < 3.0) + // exam not passed, reset points for this subject + ectsPoints[pointsPair] = 0.0f + else if (pointsPair !in ectsPoints) + // no points for this subject, simply assign + ectsPoints[pointsPair] = grade.weight + + if (filterTermId != null && grade.comment != filterTermId) + continue + + if (universityAverageMode == UNIVERSITY_AVERAGE_MODE_ECTS) + semesterSum.add(grade.value * grade.weight) + else + semesterSum.add(grade.value) + semesterCount.add(grade.weight) + } + when (universityAverageMode) { + UNIVERSITY_AVERAGE_MODE_SIMPLE -> { + stats.universitySem = semesterSum.sum() / semesterCount.size + stats.universityTotal = totalSum.sum() / totalCount.size + } + UNIVERSITY_AVERAGE_MODE_ECTS -> { + stats.universitySem = semesterSum.sum() / semesterCount.sum() + stats.universityTotal = totalSum.sum() / totalCount.sum() + } + } + stats.universityEcts = ectsPoints.values.sum() + return (items + stats).toMutableList() + } + val sem1Expected = mutableListOf<Float>() val sem2Expected = mutableListOf<Float>() val yearlyExpected = mutableListOf<Float>() @@ -302,11 +402,6 @@ class GradesListFragment : BaseFragment<GradesListFragmentBinding, MainActivity> stats.pointSem2 = sem2Point.averageOrNull()?.toFloat() ?: 0f stats.pointYearly = yearlyPoint.averageOrNull()?.toFloat() ?: 0f - when (manager.orderBy) { - GradesManager.ORDER_BY_DATE_DESC -> items.sortByDescending { it.lastAddedDate } - GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate } - } - return (items + stats).toMutableList() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt index 26041dcd..2d8f04e9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt @@ -22,4 +22,8 @@ class GradesStats { var pointSem1 = 0f var pointSem2 = 0f var pointYearly = 0f + + var universitySem = 0f + var universityTotal = 0f + var universityEcts = 0f } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt index 4d552722..d8f4d3af 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt @@ -11,6 +11,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.databinding.GradesItemGradeBinding import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter @@ -59,9 +60,12 @@ class GradeViewHolder( b.gradeWeight.isVisible = weightText != null b.gradeTeacherName.text = grade.teacherName - b.gradeAddedDate.text = Date.fromMillis(grade.addedDate).let { - it.getRelativeString(app, 5) ?: it.formattedStringShort - } + if (grade.addedDate == 0L || grade.type == TYPE_NO_GRADE) + b.gradeAddedDate.text = null + else + b.gradeAddedDate.text = Date.fromMillis(grade.addedDate).let { + it.getRelativeString(app, 5) ?: it.formattedStringShort + } b.unread.isVisible = grade.showAsUnseen if (!grade.seen) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt index f100c02e..eacba5fa 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt @@ -31,9 +31,35 @@ class StatsViewHolder( override fun onBind(activity: AppCompatActivity, app: App, item: GradesStats, position: Int, adapter: GradesAdapter) { val manager = app.gradesManager + val isUniversity = manager.isUniversity val showAverages = mutableListOf<Int>() val showPoint = mutableListOf<Int>() + b.universityTitle.isVisible = isUniversity + b.universityLayout.isVisible = isUniversity + b.universityDivider.isVisible = isUniversity + + if (isUniversity) { + val format = DecimalFormat("#.00") + + b.normalTitle.isVisible = false + b.normalLayout.isVisible = false + b.normalDivider.isVisible = false + b.helpButton.isVisible = false + b.pointTitle.isVisible = false + b.pointLayout.isVisible = false + b.pointDivider.isVisible = false + b.noData.isVisible = false + b.disclaimer.isVisible = true + b.customValueDivider.isVisible = false + b.customValueLayout.isVisible = false + + b.universitySemester.text = format.format(item.universitySem) + b.universityTotal.text = format.format(item.universityTotal) + b.universityEcts.text = format.format(item.universityEcts) + return + } + getSemesterString(app, item.normalSem1, item.normalSem1Proposed, item.normalSem1Final, item.sem1NotAllFinal).let { (average, notice) -> b.normalSemester1Layout.isVisible = average != null b.normalSemester1Notice.isVisible = notice != null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt index 1be8decd..ce72928b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt @@ -60,9 +60,24 @@ class SubjectViewHolder( val firstSemester = item.semesters.firstOrNull() ?: return - b.yearSummary.text = manager.getYearSummaryString(app, item.semesters.map { it.grades.size }.sum(), item.averages) + if (manager.isUniversity) { + val ectsPoints = item.semesters.firstOrNull()?.grades?.maxOf { it.weight } + b.yearSummary.text = if (ectsPoints != null) + contextWrapper.getString( + R.string.grades_ects_points_format, + ectsPoints + ) + else + null + } else { + b.yearSummary.text = manager.getYearSummaryString( + app, + item.semesters.map { it.grades.size }.sum(), + item.averages + ) + } - if (firstSemester.number != item.semester) { + if (firstSemester.number != item.semester && !manager.isUniversity) { b.gradesContainer.addView(TextView(activity).apply { setTextColor(android.R.attr.textColorSecondary.resolveAttr(context)) setText(R.string.grades_preview_other_semester, firstSemester.number) @@ -90,16 +105,18 @@ class SubjectViewHolder( )) } - b.previewContainer.addView(TextView(activity).apply { - setTextColor(android.R.attr.textColorSecondary.resolveAttr(context)) - text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number) - //gravity = Gravity.END - layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(0, 0, 8.dp, 0) - } - maxLines = 1 - ellipsize = TextUtils.TruncateAt.END - }) + if (!manager.isUniversity) { + b.previewContainer.addView(TextView(activity).apply { + setTextColor(android.R.attr.textColorSecondary.resolveAttr(context)) + text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number) + //gravity = Gravity.END + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + setMargins(0, 0, 8.dp, 0) + } + maxLines = 1 + ellipsize = TextUtils.TruncateAt.END + }) + } // add the topmost semester's grades to preview container (collapsed) firstSemester.proposedGrade?.let { @@ -137,7 +154,7 @@ class SubjectViewHolder( } // if showing semester 2, add yearly grades to preview container (collapsed) - if (firstSemester.number == item.semester) { + if (firstSemester.number == item.semester && !manager.isUniversity) { b.previewContainer.addView(TextView(activity).apply { text = manager.getAverageString(app, item.averages, nameSemester = true) layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt index 424944d1..c91c03be 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Subject import pl.szczodrzynski.edziennik.data.db.full.GradeFull @@ -71,7 +72,7 @@ class HomeGradesCard( app.db.gradeDao().getAllFromDate(profile.id, sevenDaysAgo).observe(fragment, Observer { grades.apply { clear() - addAll(it) + addAll(it.filter { it.type != TYPE_NO_GRADE }) } update() }) diff --git a/app/src/main/res/layout/dialog_config_grades.xml b/app/src/main/res/layout/dialog_config_grades.xml index d7bca752..55467b14 100644 --- a/app/src/main/res/layout/dialog_config_grades.xml +++ b/app/src/main/res/layout/dialog_config_grades.xml @@ -7,6 +7,13 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> + <data> + + <variable + name="isUniversity" + type="boolean" /> + </data> + <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> @@ -29,7 +36,8 @@ android:layout_marginTop="8dp" android:layout_marginBottom="4dp" android:gravity="center_vertical" - android:orientation="horizontal"> + android:orientation="horizontal" + android:isVisible="@{!isUniversity}"> <CheckBox android:id="@+id/customPlusCheckBox" @@ -54,7 +62,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" - android:orientation="horizontal"> + android:orientation="horizontal" + android:isVisible="@{!isUniversity}"> <CheckBox android:id="@+id/customMinusCheckBox" @@ -79,14 +88,16 @@ android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginVertical="8dp" - android:background="@drawable/divider" /> + android:background="@drawable/divider" + android:isVisible="@{!isUniversity}" /> <CheckBox android:id="@+id/dontCountGrades" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="0dp" - android:text="@string/grades_config_dont_count_grades" /> + android:text="@string/grades_config_dont_count_grades" + android:isVisible="@{!isUniversity}" /> <com.google.android.material.textfield.TextInputLayout style="?textInputOutlinedDenseStyle" @@ -95,7 +106,8 @@ android:layout_marginTop="8dp" android:enabled="@{dontCountGrades.checked}" android:hint="@string/grades_config_dont_count_hint" - app:placeholderText="@string/grades_config_dont_count_placeholder"> + app:placeholderText="@string/grades_config_dont_count_placeholder" + android:isVisible="@{!isUniversity}"> <pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit android:id="@+id/dontCountGradesText" @@ -112,11 +124,20 @@ android:minHeight="32dp" android:text="@string/grades_config_dont_show_improved" /> + <CheckBox + android:id="@+id/hideNoGrade" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="32dp" + android:text="@string/grades_config_hide_no_grade" + android:isVisible="@{isUniversity}" /> + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:orientation="horizontal"> + android:orientation="horizontal" + android:isVisible="@{!isUniversity}"> <CheckBox android:id="@+id/averageWithoutWeight" @@ -138,6 +159,14 @@ tools:src="@android:drawable/ic_menu_help" /> </LinearLayout> + <CheckBox + android:id="@+id/countEctsInProgress" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="32dp" + android:text="@string/grades_config_ects_in_progress" + android:isVisible="@{isUniversity}" /> + <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -197,12 +226,14 @@ android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/menu_grades_average_mode" - android:textAppearance="?textAppearanceTitleMedium" /> + android:textAppearance="?textAppearanceTitleMedium" + android:isVisible="@{!isUniversity}" /> <RadioGroup android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="8dp"> + android:layout_marginTop="8dp" + android:isVisible="@{!isUniversity}"> <RadioButton android:id="@+id/gradeAverageMode4" @@ -239,6 +270,35 @@ android:minHeight="0dp" android:text="@string/settings_register_avg_mode_3" /> </RadioGroup> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="4dp" + style="@style/TextAppearance.AppCompat.Small" + android:text="@string/menu_grades_university_average_mode" + android:isVisible="@{isUniversity}" /> + + <RadioGroup + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:isVisible="@{isUniversity}"> + + <RadioButton + android:id="@+id/gradeUniversityAverageMode1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="0dp" + android:text="@string/settings_register_university_avg_mode_1"/> + + <RadioButton + android:id="@+id/gradeUniversityAverageMode0" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="0dp" + android:text="@string/settings_register_university_avg_mode_0"/> + </RadioGroup> </LinearLayout> </ScrollView> </layout> diff --git a/app/src/main/res/layout/grades_item_stats.xml b/app/src/main/res/layout/grades_item_stats.xml index 60187645..adb1fd40 100644 --- a/app/src/main/res/layout/grades_item_stats.xml +++ b/app/src/main/res/layout/grades_item_stats.xml @@ -335,6 +335,105 @@ android:layout_marginTop="8dp" android:background="@drawable/divider" android:visibility="gone" /> + + <TextView + android:id="@+id/universityTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="8dp" + android:text="@string/grades_stats_university" + android:textAppearance="@style/NavView.TextView.Subtitle" /> + + <LinearLayout + android:id="@+id/universityLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingHorizontal="8dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="center_horizontal" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/grades_stats_this_semester" /> + + <TextView + android:id="@+id/universitySemester" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="sans-serif-light" + android:textSize="24sp" + tools:text="4,56" /> + </LinearLayout> + + <View + android:layout_width="1dp" + android:layout_height="match_parent" + android:layout_marginTop="8dp" + android:background="@drawable/divider" /> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="center_horizontal" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/grades_stats_all_grades" /> + + <TextView + android:id="@+id/universityTotal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="sans-serif-light" + android:textSize="24sp" + tools:text="4,67" /> + </LinearLayout> + + <View + android:layout_width="1dp" + android:layout_height="match_parent" + android:layout_marginTop="8dp" + android:background="@drawable/divider" /> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="center_horizontal" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/grades_stats_ects_points" /> + + <TextView + android:id="@+id/universityEcts" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="sans-serif-light" + android:textSize="24sp" + tools:text="120" /> + </LinearLayout> + + </LinearLayout> + + <View + android:id="@+id/universityDivider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginTop="8dp" + android:background="@drawable/divider" /> <TextView android:id="@+id/disclaimer" diff --git a/app/src/main/res/layout/grades_list_fragment.xml b/app/src/main/res/layout/grades_list_fragment.xml index f262b78d..4e2adb31 100644 --- a/app/src/main/res/layout/grades_list_fragment.xml +++ b/app/src/main/res/layout/grades_list_fragment.xml @@ -28,12 +28,38 @@ android:visibility="gone" app:drawableTopCompat="@drawable/ic_no_grades" tools:visibility="visible" /> - - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/list" + + <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="gone" - tools:listitem="@layout/grades_item_subject" - tools:visibility="visible" /> + android:orientation="vertical"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/semesterLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="8dp" + android:layout_marginVertical="8dp" + android:hint="@string/title_semester" + android:visibility="gone" + tools:visibility="visible"> + + <pl.szczodrzynski.edziennik.utils.TextInputDropDown + android:id="@+id/semesterDropdown" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="true" + android:focusableInTouchMode="true" + tools:text="Semestr zimowy 2024/2025" /> + </com.google.android.material.textfield.TextInputLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + tools:listitem="@layout/grades_item_subject" + tools:visibility="visible" /> + </LinearLayout> </FrameLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d485468..d11b9288 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -446,9 +446,12 @@ <string name="grades_config_dont_count_hint">Oceny oddziel przecinkiem</string> <string name="grades_config_dont_count_placeholder">Podaj oceny…</string> <string name="grades_config_dont_show_improved">Ukrywaj oceny poprawione z listy</string> + <string name="grades_config_ects_in_progress">Licz ECTS przed zaliczeniem całego przedmiotu</string> + <string name="grades_config_hide_no_grade">Ukrywaj pozycje \"brak oceny\"</string> <string name="grades_config_minus_value">Własna wartość minusa</string> <string name="grades_config_plus_value">Własna wartość plusa</string> <string name="grades_config_title">Konfiguracja ocen</string> + <string name="grades_ects_points_format">Punkty ECTS: %#.2f</string> <string name="grades_editor_add_grade">Dodaj ocenę</string> <string name="grades_editor_add_grade_title">Dodawanie oceny</string> <string name="grades_editor_add_grade_weight">Podaj wagę oceny</string> @@ -475,9 +478,11 @@ <string name="grades_semester_average_point_format">semestr %d: %spkt</string> <string name="grades_semester_format">Semestr %d</string> <string name="grades_semester_header_format">Semestr %d</string> + <string name="grades_stats_all_grades">wszystkie oceny</string> <string name="grades_stats_custom_config_notice">Aktualne ustawienia ocen mogą wpływać na średnią. Jeśli uważasz, że się ona nie zgadza, kliknij Konfiguruj.</string> <string name="grades_stats_custom_value_notice">Została ustawiona własna wartość plusa/minusa. Jeśli uważasz, że się ona nie zgadza, kliknij Konfiguruj.</string> <string name="grades_stats_disclaimer">*średnie ocen są poglądowe i mogą się różnić, w zależności od ustawień szkoły</string> + <string name="grades_stats_ects_points">punkty ECTS</string> <string name="grades_stats_expected">*przewidywana średnia</string> <string name="grades_stats_from_final">*z ocen końcowych\nPrzewidywana: %s</string> <string name="grades_stats_from_final_no_expected">*z ocen końcowych</string> @@ -491,11 +496,14 @@ <string name="grades_stats_proposed_avg">Śr. ocen proponowanych:\n%s</string> <string name="grades_stats_semester_1">semestr 1</string> <string name="grades_stats_semester_2">semestr 2</string> + <string name="grades_stats_this_semester">ten semestr</string> <string name="grades_stats_title">Statystyka ocen</string> + <string name="grades_stats_university">Średnia ocen za studia</string> <string name="grades_stats_yearly">całoroczna</string> <string name="grades_value_format">wartość: %s</string> <string name="grades_weight_format">waga %s</string> <string name="grades_weight_not_counted">nie liczona do śr.</string> + <string name="grades_weight_no_grade">brak oceny</string> <string name="grades_year_average_format">koniec roku: %s</string> <string name="grades_year_average_percent_format">koniec roku: %s%%</string> <string name="grades_year_average_point_format">koniec roku: %spkt</string> @@ -690,6 +698,7 @@ <string name="menu_grades_config">Ustawienia ocen</string> <string name="menu_grades_editor">Symulator edycji ocen</string> <string name="menu_grades_sort_mode">Sortuj oceny</string> + <string name="menu_grades_university_average_mode">Sposób obliczania średniej za studia</string> <string name="menu_help">Pomoc</string> <string name="menu_home_page">Strona główna</string> <string name="menu_homework">Zadania domowe</string> @@ -971,6 +980,8 @@ <string name="settings_register_dont_count_zero_text">Nie wliczaj oceny 0 do średniej</string> <string name="settings_register_login_not_implemented_text">Ten e-dziennik nie został jeszcze zaimplementowany w aplikacji.</string> <string name="settings_register_show_teacher_absences_text">Pokazuj nieobecności nauczycieli w Terminarzu</string> + <string name="settings_register_university_avg_mode_0">Średnia arytmetyczna ocen</string> + <string name="settings_register_university_avg_mode_1">Średnia ważona ocen (ECTS = waga)</string> <string name="settings_sync_customize_endpoint_announcements">Tablica ogłoszeń</string> <string name="settings_sync_customize_endpoint_attendance">Obecności/nieobecności</string> <string name="settings_sync_customize_endpoint_class_free_days">Dni wolne klasy</string>