Merge branch 'develop-v4'

This commit is contained in:
Kuba Szczodrzyński 2025-02-02 17:40:42 +01:00
commit 54693bf25e
No known key found for this signature in database
GPG Key ID: 43037AC62A600562
36 changed files with 878 additions and 97 deletions

View File

@ -183,7 +183,7 @@ jobs:
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
- name: Upload workflow artifact - name: Upload workflow artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: ${{ steps.changelog.outputs.appVersionName }} name: ${{ steps.changelog.outputs.appVersionName }}

View File

@ -1,7 +1,7 @@
name: Release name: Release
on: on:
push: push:
tags: ["v*.*.*"] tags: ["v*.*"]
jobs: jobs:
build: build:
name: Build release (APK) name: Build release (APK)

View File

@ -1,8 +1,11 @@
<h3>Wersja 4.13.7, 2024-07-08</h3> <h3>Wersja 4.14, 2025-02-02</h3>
<ul> <ul>
<li>Dodano opcję uruchomienia aplikacji bez logowania.</li> <li>USOS: <b>dodano obsługę ocen</b>.</li>
<li>USOS: obliczanie średniej za studia oraz punktów ECTS.</li>
<li>USOS: poprawiono brak planu zajęć po rozpoczęciu roku.</li>
<li>Wyłączono archiwizator profili.</li>
</ul> </ul>
<br> <br>
<br> <br>
Dzięki za korzystanie ze Szkolnego!<br> Dzięki za korzystanie ze Szkolnego!<br>
<i>&copy; [Kuba Szczodrzyński](@kuba2k2) 2023</i> <i>&copy; [Kuba Szczodrzyński](@kuba2k2) 2025</i>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/ /*secret password - removed for source code publication*/
static toys AES_IV[16] = { static toys AES_IV[16] = {
0x0e, 0x87, 0x6d, 0xaa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; 0xee, 0x23, 0xf1, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -241,6 +241,14 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
config.migrate(this@App) config.migrate(this@App)
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) SSLProviderInstaller.install(applicationContext, this@App::buildHttp)
if (config.devModePassword != null) if (config.devModePassword != null)

View File

@ -43,6 +43,7 @@ class Config(db: AppDb) : BaseConfig(db) {
var appInstalledTime by config<Long>(0L) var appInstalledTime by config<Long>(0L)
var appRateSnackbarTime by config<Long>(0L) var appRateSnackbarTime by config<Long>(0L)
var appVersion by config<Int>(BuildConfig.VERSION_CODE) var appVersion by config<Int>(BuildConfig.VERSION_CODE)
var appVersionCore by config<Int>(0)
var validation by config<String?>(null, "buildValidation") var validation by config<String?>(null, "buildValidation")
var archiverEnabled by config<Boolean>(true) var archiverEnabled by config<Boolean>(true)

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
@ -15,8 +16,11 @@ class ProfileConfigGrades(base: ProfileConfig) {
var dontCountEnabled by base.config<Boolean>(false) var dontCountEnabled by base.config<Boolean>(false)
var dontCountGrades by base.config<List<String>> { listOf() } var dontCountGrades by base.config<List<String>> { listOf() }
var hideImproved by base.config<Boolean>(false) var hideImproved by base.config<Boolean>(false)
var hideNoGrade by base.config<Boolean>(false)
var hideSticksFromOld by base.config<Boolean>(false) var hideSticksFromOld by base.config<Boolean>(false)
var minusValue by base.config<Float?>(null) var minusValue by base.config<Float?>(null)
var plusValue by base.config<Float?>(null) var plusValue by base.config<Float?>(null)
var yearAverageMode by base.config<Int>(YEAR_ALL_GRADES) var yearAverageMode by base.config<Int>(YEAR_ALL_GRADES)
var universityAverageMode by base.config<Int>(UNIVERSITY_AVERAGE_MODE_ECTS)
var countEctsInProgress by base.config<Boolean>(false)
} }

View File

@ -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) { edziennikInterface = when (loginStore.type) {
LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback) LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback)
LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback) LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback)

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.usos package pl.szczodrzynski.edziennik.data.api.edziennik.usos
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
@ -73,4 +74,9 @@ class DataUsos(
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
set(value) { profile["studentId"] = value; mStudentId = value } set(value) { profile["studentId"] = value; mStudentId = value }
private var mStudentId: Int? = null 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
} }

View File

@ -10,10 +10,12 @@ import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.data.db.enums.LoginType
const val ENDPOINT_USOS_API_USER = 7000 const val ENDPOINT_USOS_API_USER = 7000
const val ENDPOINT_USOS_API_TERMS = 7010 const val ENDPOINT_USOS_API_TERMS = 7010
const val ENDPOINT_USOS_API_COURSES = 7020 const val ENDPOINT_USOS_API_COURSES = 7020
const val ENDPOINT_USOS_API_TIMETABLE = 7030 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( val UsosFeatures = listOf(
/* /*
@ -39,4 +41,12 @@ val UsosFeatures = listOf(
Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf( Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf(
ENDPOINT_USOS_API_TIMETABLE to LoginMethod.USOS_API, 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,
)),
) )

View File

@ -8,6 +8,8 @@ import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.* 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.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.UsosApiTerms
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser
@ -58,6 +60,14 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_timetable) data.startProgress(R.string.edziennik_progress_endpoint_timetable)
UsosApiTimetable(data, lastSync, onSuccess) 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) else -> onSuccess(endpointId)
} }
} }

View File

@ -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.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES 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.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.data.db.entity.Team
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
@ -25,17 +26,20 @@ class UsosApiCourses(
apiRequest<JsonObject>( apiRequest<JsonObject>(
tag = TAG, tag = TAG,
service = "courses/user", service = "courses/user",
params = mapOf(
"active_terms_only" to false,
),
fields = listOf( fields = listOf(
// "terms" to listOf("id", "name", "start_date", "end_date"), // "terms" to listOf("id", "name", "start_date", "end_date"),
"course_editions" to listOf( "course_editions" to listOf(
"course_id", "course_id",
"course_name", "course_name",
// "term_id",
"user_groups" to listOf( "user_groups" to listOf(
"course_unit_id", "course_unit_id",
"group_number", "group_number",
// "class_type", "class_type",
"class_type_id", "class_type_id",
"term_id",
"lecturers", "lecturers",
), ),
), ),
@ -63,22 +67,38 @@ class UsosApiCourses(
for (courseEdition in courseEditions) { for (courseEdition in courseEditions) {
val courseId = courseEdition.getString("course_id") ?: continue val courseId = courseEdition.getString("course_id") ?: continue
val courseName = courseEdition.getLangString("course_name") ?: 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) { for (userGroup in userGroups) {
val courseUnitId = userGroup.getLong("course_unit_id") ?: continue val courseUnitId = userGroup.getLong("course_unit_id") ?: continue
val groupNumber = userGroup.getInt("group_number") ?: 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 classTypeId = userGroup.getString("class_type_id") ?: continue
val termId = userGroup.getString("term_id") ?: continue
val lecturers = userGroup.getLecturerIds("lecturers") val lecturers = userGroup.getLecturerIds("lecturers")
data.teamList.put(courseUnitId, Team( data.teamList.put(
profileId, courseUnitId, Team(
courseUnitId, profileId,
"${profile?.studentClassName} $classTypeId$groupNumber - $courseName", courseUnitId,
2, "${profile?.studentClassName} $courseName ($classTypeId$groupNumber)",
"${data.schoolId}:${courseId} $classTypeId$groupNumber", 2,
lecturers.firstOrNull() ?: -1L, "${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 hasValidTeam = true
} }
} }

View File

@ -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
}
}

View File

@ -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,
)
)
}
}
}

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonArray 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.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS
@ -16,43 +17,81 @@ class UsosApiTerms(
override val data: DataUsos, override val data: DataUsos,
override val lastSync: Long?, override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit, val onSuccess: (endpointId: Int) -> Unit,
names: Set<String>? = null,
) : UsosApi(data, lastSync) { ) : UsosApi(data, lastSync) {
companion object { companion object {
const val TAG = "UsosApiTerms" const val TAG = "UsosApiTerms"
} }
init { init {
apiRequest<JsonArray>( if (names != null) {
tag = TAG, apiRequest<JsonObject>(
service = "terms/search", tag = TAG,
params = mapOf( service = "terms/terms",
"query" to Date.getToday().year.toString(), params = mapOf("term_ids" to names.joinToString("|")),
), responseType = ResponseType.OBJECT,
responseType = ResponseType.ARRAY, ) { json, response ->
) { json, response -> if (!processResponse(json.entrySet().map { it.value.asJsonObject })) {
if (!processResponse(json)) { data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) return@apiRequest
return@apiRequest }
}
data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY) data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY)
onSuccess(ENDPOINT_USOS_API_TERMS) 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() 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)) if (!term.getBoolean("is_active", false))
continue continue
val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } ?: 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 val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue
if (today in startDate..finishDate) { if (today !in startDate..finishDate)
profile?.studentSchoolYearStart = startDate.year continue
profile?.dateSemester1Start = startDate
profile?.dateSemester2Start = finishDate 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 return true
} }
} }

View File

@ -32,7 +32,8 @@ class UsosApiUser(
"last_name", "last_name",
"student_number", "student_number",
"student_programmes" to listOf( "student_programmes" to listOf(
"programme" to listOf("id"), "id",
"programme" to listOf("id", "description"),
), ),
), ),
), ),
@ -40,9 +41,11 @@ class UsosApiUser(
) { json, response -> ) { json, response ->
val programmes = json.getJsonArray("student_programmes") val programmes = json.getJsonArray("student_programmes")
if (programmes.isNullOrEmpty()) { if (programmes.isNullOrEmpty()) {
data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) data.error(
.withApiResponse(json) ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
.withResponse(response)) .withApiResponse(json)
.withResponse(response)
)
return@apiRequest return@apiRequest
} }
@ -50,13 +53,19 @@ class UsosApiUser(
val lastName = json.getString("last_name") val lastName = json.getString("last_name")
val studentName = buildFullName(firstName, lastName) 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 data.studentId = json.getInt("id") ?: data.studentId
profile?.studentNameLong = studentName profile?.studentNameLong = studentName
profile?.studentNameShort = studentName.getShortName() profile?.studentNameShort = studentName.getShortName()
profile?.studentNumber = json.getInt("student_number", -1) profile?.studentNumber = json.getInt("student_number", -1)
profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id") profile?.studentClassName = programmeId
profile?.studentClassName?.let { val team = programmeId?.let {
data.getTeam( data.getTeam(
id = null, id = null,
name = it, name = it,
@ -64,6 +73,7 @@ class UsosApiUser(
isTeamClass = true, isTeamClass = true,
) )
} }
team?.code = "${data.schoolId}:${studentProgrammeId}:${programmeId}"
data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY) data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY)
onSuccess(ENDPOINT_USOS_API_USER) onSuccess(ENDPOINT_USOS_API_USER)

View File

@ -206,7 +206,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
teams.filter { it.profileId == profile.id }.map { it.code } teams.filter { it.profileId == profile.id }.map { it.code }
) )
val hash = user.toString().md5() val hash = user.toString().md5()
if (hash == profile.config.hash) if (hash == profile.config.hash && app.config.hash != "invalid")
return@mapNotNull null return@mapNotNull null
return@mapNotNull user to profile.config return@mapNotNull user to profile.config
} }

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/ /*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MD0WAYwfGc===.$param2".sha256() return "$param1.MTIzNDU2Nzg5MDADAoYzGn===.$param2".sha256()
} }
} }

View File

@ -24,4 +24,7 @@ interface EndpointTimerDao {
@Query("DELETE FROM endpointTimers WHERE profileId = :profileId") @Query("DELETE FROM endpointTimers WHERE profileId = :profileId")
fun clear(profileId: Int) fun clear(profileId: Int)
@Query("DELETE FROM endpointTimers")
fun clear()
} }

View File

@ -55,6 +55,7 @@ open class Grade(
const val TYPE_DESCRIPTIVE = 30 const val TYPE_DESCRIPTIVE = 30
const val TYPE_DESCRIPTIVE_TEXT = 31 const val TYPE_DESCRIPTIVE_TEXT = 31
const val TYPE_TEXT = 40 const val TYPE_TEXT = 40
const val TYPE_NO_GRADE = 100
} }
@ColumnInfo(name = "gradeValueMax") @ColumnInfo(name = "gradeValueMax")

View File

@ -93,6 +93,7 @@ internal val FEATURES_PODLASIE = setOf(
internal val FEATURES_USOS = setOf( internal val FEATURES_USOS = setOf(
TIMETABLE, TIMETABLE,
AGENDA, AGENDA,
GRADES,
STUDENT_INFO, STUDENT_INFO,
STUDENT_NUMBER, STUDENT_NUMBER,

View File

@ -72,8 +72,7 @@ fun Profile.getAppData() =
if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType) if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType)
fun Profile.shouldArchive(): Boolean { fun Profile.shouldArchive(): Boolean {
if (loginStoreType == LoginType.DEMO) return false
return false
// vulcan hotfix // vulcan hotfix
if (dateYearEnd.month > 6) { if (dateYearEnd.month > 6) {
dateYearEnd.month = 6 dateYearEnd.month = 6

View File

@ -21,6 +21,8 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_M
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED 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_DATE_DESC
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_SUBJECT_ASC 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_AVG
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM 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_AVG
@ -47,6 +49,8 @@ class GradesConfigDialog(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override suspend fun loadConfig() { override suspend fun loadConfig() {
b.isUniversity = app.gradesManager.isUniversity
b.customPlusCheckBox.isChecked = app.profile.config.grades.plusValue != null b.customPlusCheckBox.isChecked = app.profile.config.grades.plusValue != null
b.customPlusValue.isVisible = b.customPlusCheckBox.isChecked b.customPlusValue.isVisible = b.customPlusCheckBox.isChecked
b.customMinusCheckBox.isChecked = app.profile.config.grades.minusValue != null b.customMinusCheckBox.isChecked = app.profile.config.grades.minusValue != null
@ -76,10 +80,18 @@ class GradesConfigDialog(
else -> null else -> null
}?.isChecked = true }?.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 = b.dontCountGrades.isChecked =
app.profile.config.grades.dontCountEnabled && app.profile.config.grades.dontCountGrades.isNotEmpty() app.profile.config.grades.dontCountEnabled && app.profile.config.grades.dontCountGrades.isNotEmpty()
b.hideImproved.isChecked = app.profile.config.grades.hideImproved 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.averageWithoutWeight.isChecked = app.profile.config.grades.averageWithoutWeight
b.countEctsInProgress.isChecked = app.profile.config.grades.countEctsInProgress
if (app.profile.config.grades.dontCountGrades.isEmpty()) { if (app.profile.config.grades.dontCountGrades.isEmpty()) {
b.dontCountGradesText.setText("nb, 0, bz, bd") b.dontCountGradesText.setText("nb, 0, bz, bd")
@ -149,10 +161,21 @@ class GradesConfigDialog(
app.profile.config.grades.yearAverageMode = YEAR_1_SEM_2_SEM 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.hideImproved.onChange { _, isChecked -> app.profile.config.grades.hideImproved = isChecked }
b.hideNoGrade.onChange { _, isChecked -> app.profile.config.grades.hideNoGrade = isChecked }
b.averageWithoutWeight.onChange { _, isChecked -> b.averageWithoutWeight.onChange { _, isChecked ->
app.profile.config.grades.averageWithoutWeight = isChecked app.profile.config.grades.averageWithoutWeight = isChecked
} }
b.countEctsInProgress.onChange { _, isChecked ->
app.profile.config.grades.countEctsInProgress = isChecked
}
b.averageWithoutWeightHelp.onClick { b.averageWithoutWeightHelp.onClick {
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)

View File

@ -16,6 +16,7 @@ import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Grade 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.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.ext.onClick
import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
@ -134,6 +135,7 @@ class GradesAdapter(
if (model.state == STATE_CLOSED) { if (model.state == STATE_CLOSED) {
val subItems = when { val subItems = when {
model is GradesSubject && manager.isUniversity -> listOf()
model is GradesSemester && model.grades.isEmpty() -> model is GradesSemester && model.grades.isEmpty() ->
listOf(GradesEmpty()) listOf(GradesEmpty())
model is GradesSemester && manager.hideImproved -> model is GradesSemester && manager.hideImproved ->
@ -147,10 +149,12 @@ class GradesAdapter(
if (notifyAdapter) notifyItemInserted(position) if (notifyAdapter) notifyItemInserted(position)
} }
position++
model.state = STATE_OPENED model.state = STATE_OPENED
items.addAll(position, subItems.filterNotNull()) if (subItems.isNotEmpty()) {
if (notifyAdapter) notifyItemRangeInserted(position, subItems.size) position++
items.addAll(position, subItems.filterNotNull())
if (notifyAdapter) notifyItemRangeInserted(position, subItems.size)
}
if (model is GradesSubject) { if (model is GradesSubject) {
// auto expand first semester // 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) holder.itemView.setOnClickListener(onClickListener)
else holder.itemView.isEnabled = true
} else {
holder.itemView.setOnClickListener(null) holder.itemView.setOnClickListener(null)
holder.itemView.isEnabled = false
}
} }
fun notifyItemChanged(model: Any) { fun notifyItemChanged(model: Any) {

View File

@ -18,6 +18,7 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
import kotlinx.coroutines.* import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Grade 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.enums.MetadataType
import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding
@ -28,7 +29,10 @@ import pl.szczodrzynski.edziennik.ui.grades.models.GradesAverages
import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester
import pl.szczodrzynski.edziennik.ui.grades.models.GradesStats import pl.szczodrzynski.edziennik.ui.grades.models.GradesStats
import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject 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
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 pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -85,6 +89,35 @@ class GradesListFragment : Fragment(), CoroutineScope {
else -> grades 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 // load & configure the adapter
adapter.items = withContext(Dispatchers.Default) { processGrades(items) } adapter.items = withContext(Dispatchers.Default) { processGrades(items) }
if (items.isNotNullNorEmpty() && b.list.adapter == null) { if (items.isNotNullNorEmpty() && b.list.adapter == null) {
@ -188,13 +221,23 @@ class GradesListFragment : Fragment(), CoroutineScope {
var semesterNumber = 0 var semesterNumber = 0
var subject = GradesSubject(subjectId, "") var subject = GradesSubject(subjectId, "")
var semester = GradesSemester(0, 1) 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 // grades returned by the query are ordered
// by the subject ID, so it's easier and probably // by the subject ID, so it's easier and probably
// a bit faster to build all the models // a bit faster to build all the models
for (grade in grades) { 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) /*if (grade.parentId != null && grade.parentId != -1L)
continue // the grade is hidden as a new, improved one is available*/ continue // the grade is hidden as a new, improved one is available*/
if (grade.subjectId != subjectId) { if (grade.subjectId != subjectId) {
@ -258,8 +301,63 @@ class GradesListFragment : Fragment(), CoroutineScope {
subject.lastAddedDate = max(subject.lastAddedDate, grade.addedDate) 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() 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 sem1Expected = mutableListOf<Float>()
val sem2Expected = mutableListOf<Float>() val sem2Expected = mutableListOf<Float>()
val yearlyExpected = mutableListOf<Float>() val yearlyExpected = mutableListOf<Float>()
@ -330,11 +428,6 @@ class GradesListFragment : Fragment(), CoroutineScope {
stats.pointSem2 = sem2Point.averageOrNull()?.toFloat() ?: 0f stats.pointSem2 = sem2Point.averageOrNull()?.toFloat() ?: 0f
stats.pointYearly = yearlyPoint.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() return (items + stats).toMutableList()
} }

View File

@ -22,4 +22,8 @@ class GradesStats {
var pointSem1 = 0f var pointSem1 = 0f
var pointSem2 = 0f var pointSem2 = 0f
var pointYearly = 0f var pointYearly = 0f
var universitySem = 0f
var universityTotal = 0f
var universityEcts = 0f
} }

View File

@ -11,6 +11,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R 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.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.databinding.GradesItemGradeBinding import pl.szczodrzynski.edziennik.databinding.GradesItemGradeBinding
import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter
@ -59,9 +60,12 @@ class GradeViewHolder(
b.gradeWeight.isVisible = weightText != null b.gradeWeight.isVisible = weightText != null
b.gradeTeacherName.text = grade.teacherName b.gradeTeacherName.text = grade.teacherName
b.gradeAddedDate.text = Date.fromMillis(grade.addedDate).let { if (grade.addedDate == 0L || grade.type == TYPE_NO_GRADE)
it.getRelativeString(app, 5) ?: it.formattedStringShort b.gradeAddedDate.text = null
} else
b.gradeAddedDate.text = Date.fromMillis(grade.addedDate).let {
it.getRelativeString(app, 5) ?: it.formattedStringShort
}
b.unread.isVisible = grade.showAsUnseen b.unread.isVisible = grade.showAsUnseen
if (!grade.seen) { if (!grade.seen) {

View File

@ -31,9 +31,35 @@ class StatsViewHolder(
override fun onBind(activity: AppCompatActivity, app: App, item: GradesStats, position: Int, adapter: GradesAdapter) { override fun onBind(activity: AppCompatActivity, app: App, item: GradesStats, position: Int, adapter: GradesAdapter) {
val manager = app.gradesManager val manager = app.gradesManager
val isUniversity = manager.isUniversity
val showAverages = mutableListOf<Int>() val showAverages = mutableListOf<Int>()
val showPoint = 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) -> getSemesterString(app, item.normalSem1, item.normalSem1Proposed, item.normalSem1Final, item.sem1NotAllFinal).let { (average, notice) ->
b.normalSemester1Layout.isVisible = average != null b.normalSemester1Layout.isVisible = average != null
b.normalSemester1Notice.isVisible = notice != null b.normalSemester1Notice.isVisible = notice != null

View File

@ -62,9 +62,24 @@ class SubjectViewHolder(
val firstSemester = item.semesters.firstOrNull() ?: return 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(contextWrapper).apply { b.gradesContainer.addView(TextView(contextWrapper).apply {
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context)) setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
setText(R.string.grades_preview_other_semester, firstSemester.number) setText(R.string.grades_preview_other_semester, firstSemester.number)
@ -92,16 +107,18 @@ class SubjectViewHolder(
)) ))
} }
b.previewContainer.addView(TextView(contextWrapper).apply { if (!manager.isUniversity) {
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context)) b.previewContainer.addView(TextView(contextWrapper).apply {
text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number) setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
//gravity = Gravity.END text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number)
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { //gravity = Gravity.END
setMargins(0, 0, 8.dp, 0) layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
} setMargins(0, 0, 8.dp, 0)
maxLines = 1 }
ellipsize = TextUtils.TruncateAt.END maxLines = 1
}) ellipsize = TextUtils.TruncateAt.END
})
}
// add the topmost semester's grades to preview container (collapsed) // add the topmost semester's grades to preview container (collapsed)
firstSemester.proposedGrade?.let { firstSemester.proposedGrade?.let {
@ -139,7 +156,7 @@ class SubjectViewHolder(
} }
// if showing semester 2, add yearly grades to preview container (collapsed) // 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(contextWrapper).apply { b.previewContainer.addView(TextView(contextWrapper).apply {
text = manager.getAverageString(app, item.averages, nameSemester = true) text = manager.getAverageString(app, item.averages, nameSemester = true)
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {

View File

@ -25,6 +25,7 @@ import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R 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.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Subject import pl.szczodrzynski.edziennik.data.db.entity.Subject
import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.data.db.full.GradeFull
@ -70,7 +71,7 @@ class HomeGradesCard(
app.db.gradeDao().getAllFromDate(profile.id, sevenDaysAgo).observe(fragment, Observer { app.db.gradeDao().getAllFromDate(profile.id, sevenDaysAgo).observe(fragment, Observer {
grades.apply { grades.apply {
clear() clear()
addAll(it) addAll(it.filter { it.type != TYPE_NO_GRADE })
} }
update() update()
}) })

View File

@ -12,9 +12,11 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Grade 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_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_AVG
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_SUM 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.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.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.ext.asColoredSpannable import pl.szczodrzynski.edziennik.ext.asColoredSpannable
import pl.szczodrzynski.edziennik.ext.get import pl.szczodrzynski.edziennik.ext.get
@ -42,6 +44,8 @@ class GradesManager(val app: App) : CoroutineScope {
const val YEAR_ALL_GRADES = 4 const val YEAR_ALL_GRADES = 4
const val COLOR_MODE_DEFAULT = 0 const val COLOR_MODE_DEFAULT = 0
const val COLOR_MODE_WEIGHTED = 1 const val COLOR_MODE_WEIGHTED = 1
const val UNIVERSITY_AVERAGE_MODE_SIMPLE = 0
const val UNIVERSITY_AVERAGE_MODE_ECTS = 1
} }
private val job = Job() private val job = Job()
@ -69,6 +73,8 @@ class GradesManager(val app: App) : CoroutineScope {
get() = app.profile.config.grades.hideImproved get() = app.profile.config.grades.hideImproved
val averageWithoutWeight val averageWithoutWeight
get() = app.profile.config.grades.averageWithoutWeight get() = app.profile.config.grades.averageWithoutWeight
val isUniversity
get() = app.profile.loginStoreType.schoolType == SchoolType.UNIVERSITY
fun getOrderByString() = when (orderBy) { fun getOrderByString() = when (orderBy) {
@ -87,6 +93,7 @@ class GradesManager(val app: App) : CoroutineScope {
else else
context.getString(R.string.grades_weight_format, format.format(grade.weight)) 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_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 else -> null
} }
@ -158,6 +165,19 @@ class GradesManager(val app: App) : CoroutineScope {
} }
} }
type == TYPE_NORMAL && defColor -> grade.color and 0xffffff 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 -> { type in TYPE_NORMAL..TYPE_YEAR_FINAL -> {
when (grade.name.lowercase()) { when (grade.name.lowercase()) {
"+", "++", "+++" -> 0x4caf50 "+", "++", "+++" -> 0x4caf50

View File

@ -6,6 +6,14 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="isUniversity"
type="boolean" />
</data>
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -19,16 +27,18 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="16dp" android:layout_marginTop="16dp"
style="@style/TextAppearance.AppCompat.Small" style="@style/TextAppearance.AppCompat.Small"
android:text="@string/grades_config_title"/> android:text="@string/grades_config_title"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:isVisible="@{!isUniversity}">
<CheckBox <CheckBox
android:id="@+id/customPlusCheckBox" android:id="@+id/customPlusCheckBox"
@ -54,7 +64,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="6dp" android:layout_marginBottom="6dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:isVisible="@{!isUniversity}">
<CheckBox <CheckBox
android:id="@+id/customMinusCheckBox" android:id="@+id/customMinusCheckBox"
@ -79,14 +90,16 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_marginBottom="6dp" android:layout_marginBottom="6dp"
android:background="@drawable/divider"/> android:background="@drawable/divider"
android:isVisible="@{!isUniversity}" />
<CheckBox <CheckBox
android:id="@+id/dontCountGrades" android:id="@+id/dontCountGrades"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="0dp" 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 <com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -94,7 +107,8 @@
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:hint="@string/grades_config_dont_count_hint" 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:enabled="@{dontCountGrades.checked}"> android:enabled="@{dontCountGrades.checked}"
android:isVisible="@{!isUniversity}">
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit <pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/dontCountGradesText" android:id="@+id/dontCountGradesText"
@ -111,10 +125,19 @@
android:minHeight="32dp" android:minHeight="32dp"
android:text="@string/grades_config_dont_show_improved"/> 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal"
android:isVisible="@{!isUniversity}">
<CheckBox <CheckBox
android:id="@+id/averageWithoutWeight" android:id="@+id/averageWithoutWeight"
@ -136,6 +159,14 @@
tools:src="@android:drawable/ic_menu_help" /> tools:src="@android:drawable/ic_menu_help" />
</LinearLayout> </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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -195,11 +226,13 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
style="@style/TextAppearance.AppCompat.Small" style="@style/TextAppearance.AppCompat.Small"
android:text="@string/menu_grades_average_mode"/> android:text="@string/menu_grades_average_mode"
android:isVisible="@{!isUniversity}" />
<RadioGroup <RadioGroup
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:isVisible="@{!isUniversity}">
<RadioButton <RadioButton
android:id="@+id/gradeAverageMode4" android:id="@+id/gradeAverageMode4"
@ -236,6 +269,35 @@
android:minHeight="0dp" android:minHeight="0dp"
android:text="@string/settings_register_avg_mode_3"/> android:text="@string/settings_register_avg_mode_3"/>
</RadioGroup> </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> </LinearLayout>
</ScrollView> </ScrollView>
</layout> </layout>

View File

@ -283,6 +283,105 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:background="@drawable/divider" /> android:background="@drawable/divider" />
<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 <TextView
android:id="@+id/disclaimer" android:id="@+id/disclaimer"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -37,13 +37,39 @@
app:drawableTopCompat="@drawable/ic_no_grades" app:drawableTopCompat="@drawable/ic_no_grades"
tools:visibility="visible" /> tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView <LinearLayout
android:id="@+id/list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" android:orientation="vertical">
tools:listitem="@layout/grades_item_subject"
tools:visibility="visible" /> <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> </FrameLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator> </pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout> </layout>

View File

@ -445,9 +445,12 @@
<string name="grades_config_dont_count_hint">Oceny oddziel przecinkiem</string> <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_count_placeholder">Podaj oceny…</string>
<string name="grades_config_dont_show_improved">Ukrywaj oceny poprawione z listy</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_minus_value">Własna wartość minusa</string>
<string name="grades_config_plus_value">Własna wartość plusa</string> <string name="grades_config_plus_value">Własna wartość plusa</string>
<string name="grades_config_title">Konfiguracja ocen</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">Dodaj ocenę</string>
<string name="grades_editor_add_grade_title">Dodawanie oceny</string> <string name="grades_editor_add_grade_title">Dodawanie oceny</string>
<string name="grades_editor_add_grade_weight">Podaj wagę oceny</string> <string name="grades_editor_add_grade_weight">Podaj wagę oceny</string>
@ -474,9 +477,11 @@
<string name="grades_semester_average_point_format">semestr %d: %spkt</string> <string name="grades_semester_average_point_format">semestr %d: %spkt</string>
<string name="grades_semester_format">Semestr %d</string> <string name="grades_semester_format">Semestr %d</string>
<string name="grades_semester_header_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_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_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_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_expected">*przewidywana średnia</string>
<string name="grades_stats_from_final">*z ocen końcowych\nPrzewidywana: %s</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> <string name="grades_stats_from_final_no_expected">*z ocen końcowych</string>
@ -490,11 +495,14 @@
<string name="grades_stats_proposed_avg">Śr. ocen proponowanych:\n%s</string> <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_1">semestr 1</string>
<string name="grades_stats_semester_2">semestr 2</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_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_stats_yearly">całoroczna</string>
<string name="grades_value_format">wartość: %s</string> <string name="grades_value_format">wartość: %s</string>
<string name="grades_weight_format">waga %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_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_format">koniec roku: %s</string>
<string name="grades_year_average_percent_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> <string name="grades_year_average_point_format">koniec roku: %spkt</string>
@ -689,6 +697,7 @@
<string name="menu_grades_config">Ustawienia ocen</string> <string name="menu_grades_config">Ustawienia ocen</string>
<string name="menu_grades_editor">Symulator edycji ocen</string> <string name="menu_grades_editor">Symulator edycji ocen</string>
<string name="menu_grades_sort_mode">Sortuj oceny</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_help">Pomoc</string>
<string name="menu_home_page">Strona główna</string> <string name="menu_home_page">Strona główna</string>
<string name="menu_homework">Zadania domowe</string> <string name="menu_homework">Zadania domowe</string>
@ -970,6 +979,8 @@
<string name="settings_register_dont_count_zero_text">Nie wliczaj oceny 0 do średniej</string> <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_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_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_announcements">Tablica ogłoszeń</string>
<string name="settings_sync_customize_endpoint_attendance">Obecności/nieobecności</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> <string name="settings_sync_customize_endpoint_class_free_days">Dni wolne klasy</string>

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.6.10' kotlin_version = '1.6.10'
release = [ release = [
versionName: "4.13.7", versionName: "4.14",
versionCode: 4130799 versionCode: 4140099
] ]
setup = [ setup = [