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>