forked from github/wulkanowy-mirror
Add attendance calculator (#1597)
--------- Co-authored-by: Mikołaj Pich <m.pich@outlook.com> Co-authored-by: Faierbel <RafalBO99@outlook.com>
This commit is contained in:
parent
ce09b07cfd
commit
8f5a210ec7
@ -1,11 +1,16 @@
|
|||||||
package io.github.wulkanowy.data
|
package io.github.wulkanowy.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -14,8 +19,10 @@ import kotlinx.coroutines.flow.takeWhile
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
sealed class Resource<T> {
|
sealed class Resource<out T> {
|
||||||
|
|
||||||
open class Loading<T> : Resource<T>()
|
open class Loading<T> : Resource<T>()
|
||||||
|
|
||||||
@ -64,6 +71,19 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
|
|||||||
is Resource.Error -> Resource.Error(this.error)
|
is Resource.Error -> Resource.Error(this.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <T1, T2, R> Flow<Resource<T1>>.combineWithResourceData(
|
||||||
|
flow: Flow<T2>,
|
||||||
|
crossinline block: suspend (T1, T2) -> R
|
||||||
|
): Flow<Resource<R>> =
|
||||||
|
combine(flow) { resource, inject ->
|
||||||
|
when (resource) {
|
||||||
|
is Resource.Success -> Resource.Success(block(resource.data, inject))
|
||||||
|
is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject))
|
||||||
|
is Resource.Loading -> Resource.Loading()
|
||||||
|
is Resource.Error -> Resource.Error(resource.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
|
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
|
||||||
val description = when (it) {
|
val description = when (it) {
|
||||||
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
|
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
|
||||||
@ -74,8 +94,29 @@ fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = fa
|
|||||||
Timber.i("$name: $description")
|
Timber.i("$name: $description")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T, U> Flow<Resource<T>>.mapResourceData(block: (T) -> U) = map {
|
fun <T, U> Flow<Resource<T>>.mapResourceData(block: suspend (T) -> U) = map {
|
||||||
it.mapData(block)
|
when (it) {
|
||||||
|
is Resource.Success -> Resource.Success(block(it.data))
|
||||||
|
is Resource.Intermediate -> Resource.Intermediate(block(it.data))
|
||||||
|
is Resource.Loading -> Resource.Loading()
|
||||||
|
is Resource.Error -> Resource.Error(it.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun <T, U> Flow<Resource<T>>.flatMapResourceData(
|
||||||
|
inheritIntermediate: Boolean = true, block: suspend (T) -> Flow<Resource<U>>
|
||||||
|
) = flatMapLatest {
|
||||||
|
when (it) {
|
||||||
|
is Resource.Success -> block(it.data)
|
||||||
|
is Resource.Intermediate -> block(it.data).map { newRes ->
|
||||||
|
if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data)
|
||||||
|
else newRes
|
||||||
|
}
|
||||||
|
|
||||||
|
is Resource.Loading -> flowOf(Resource.Loading())
|
||||||
|
is Resource.Error -> flowOf(Resource.Error(it.error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
|
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
|
||||||
@ -105,13 +146,13 @@ fun <T> Flow<Resource<T>>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<Resource<T>>.onResourceError(block: (Throwable) -> Unit) = onEach {
|
fun <T> Flow<Resource<T>>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
|
||||||
if (it is Resource.Error) {
|
if (it is Resource.Error) {
|
||||||
block(it.error)
|
block(it.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: () -> Unit) = onEach {
|
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
|
||||||
if (it !is Resource.Loading) {
|
if (it !is Resource.Loading) {
|
||||||
block()
|
block()
|
||||||
}
|
}
|
||||||
@ -121,6 +162,55 @@ suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it !is Resource.Loa
|
|||||||
|
|
||||||
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
|
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
|
||||||
|
|
||||||
|
// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired,
|
||||||
|
// use `debounceIntermediates` to alleviate this behavior.
|
||||||
|
inline fun <reified T> combineResourceFlows(
|
||||||
|
flows: Iterable<Flow<Resource<T>>>,
|
||||||
|
): Flow<Resource<List<T>>> = combine(flows) { items ->
|
||||||
|
var isIntermediate = false
|
||||||
|
val data = mutableListOf<T>()
|
||||||
|
for (item in items) {
|
||||||
|
when (item) {
|
||||||
|
is Resource.Success -> data.add(item.data)
|
||||||
|
is Resource.Intermediate -> {
|
||||||
|
isIntermediate = true
|
||||||
|
data.add(item.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Resource.Loading -> return@combine Resource.Loading()
|
||||||
|
is Resource.Error -> continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
// All items have to be errors for this to happen, so just return the first one.
|
||||||
|
// mapData is functionally useless and exists only to satisfy the type checker
|
||||||
|
items.first().mapData { listOf(it) }
|
||||||
|
} else if (isIntermediate) {
|
||||||
|
Resource.Intermediate(data)
|
||||||
|
} else {
|
||||||
|
Resource.Success(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
fun <T> Flow<Resource<T>>.debounceIntermediates(timeout: Duration = 5.seconds) = flow {
|
||||||
|
var wasIntermediate = false
|
||||||
|
|
||||||
|
emitAll(this@debounceIntermediates.debounce {
|
||||||
|
if (it is Resource.Intermediate) {
|
||||||
|
if (!wasIntermediate) {
|
||||||
|
wasIntermediate = true
|
||||||
|
Duration.ZERO
|
||||||
|
} else {
|
||||||
|
timeout
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wasIntermediate = false
|
||||||
|
Duration.ZERO
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
inline fun <ResultType, RequestType> networkBoundResource(
|
inline fun <ResultType, RequestType> networkBoundResource(
|
||||||
mutex: Mutex = Mutex(),
|
mutex: Mutex = Mutex(),
|
||||||
showSavedOnLoading: Boolean = true,
|
showSavedOnLoading: Boolean = true,
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package io.github.wulkanowy.data.enums
|
||||||
|
|
||||||
|
enum class AttendanceCalculatorSortingMode(private val value: String) {
|
||||||
|
ALPHABETIC("alphabetic"),
|
||||||
|
ATTENDANCE("attendance_percentage"),
|
||||||
|
LESSON_BALANCE("lesson_balance");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getByValue(value: String) =
|
||||||
|
AttendanceCalculatorSortingMode.values()
|
||||||
|
.find { it.value == value } ?: ALPHABETIC
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package io.github.wulkanowy.data.pojos
|
||||||
|
|
||||||
|
data class AttendanceData(
|
||||||
|
val subjectName: String,
|
||||||
|
val lessonBalance: Int,
|
||||||
|
val presences: Int,
|
||||||
|
val absences: Int,
|
||||||
|
) {
|
||||||
|
val total: Int
|
||||||
|
get() = presences + absences
|
||||||
|
|
||||||
|
val presencePercentage: Double
|
||||||
|
get() = if (total == 0) 0.0 else (presences.toDouble() / total) * 100
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
package io.github.wulkanowy.data.repositories
|
package io.github.wulkanowy.data.repositories
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
|
import io.github.wulkanowy.data.*
|
||||||
import io.github.wulkanowy.data.db.AppDatabase
|
import io.github.wulkanowy.data.db.AppDatabase
|
||||||
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
|
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
|
||||||
import io.github.wulkanowy.data.db.entities.Semester
|
import io.github.wulkanowy.data.db.entities.Semester
|
||||||
import io.github.wulkanowy.data.db.entities.Student
|
import io.github.wulkanowy.data.db.entities.Student
|
||||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||||
import io.github.wulkanowy.data.networkBoundResource
|
|
||||||
import io.github.wulkanowy.sdk.Sdk
|
import io.github.wulkanowy.sdk.Sdk
|
||||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||||
import io.github.wulkanowy.utils.getRefreshKey
|
import io.github.wulkanowy.utils.getRefreshKey
|
||||||
|
@ -10,6 +10,7 @@ import com.fredporciuncula.flow.preferences.Serializer
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
import io.github.wulkanowy.data.enums.AppTheme
|
import io.github.wulkanowy.data.enums.AppTheme
|
||||||
|
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||||
import io.github.wulkanowy.data.enums.GradeColorTheme
|
import io.github.wulkanowy.data.enums.GradeColorTheme
|
||||||
import io.github.wulkanowy.data.enums.GradeExpandMode
|
import io.github.wulkanowy.data.enums.GradeExpandMode
|
||||||
import io.github.wulkanowy.data.enums.GradeSortingMode
|
import io.github.wulkanowy.data.enums.GradeSortingMode
|
||||||
@ -41,6 +42,18 @@ class PreferencesRepository @Inject constructor(
|
|||||||
R.bool.pref_default_attendance_present
|
R.bool.pref_default_attendance_present
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val targetAttendanceFlow: Flow<Int>
|
||||||
|
get() = flowSharedPref.getInt(
|
||||||
|
context.getString(R.string.pref_key_attendance_target),
|
||||||
|
context.resources.getInteger(R.integer.pref_default_attendance_target)
|
||||||
|
).asFlow()
|
||||||
|
|
||||||
|
val attendanceCalculatorSortingModeFlow: Flow<AttendanceCalculatorSortingMode>
|
||||||
|
get() = flowSharedPref.getString(
|
||||||
|
context.getString(R.string.pref_key_attendance_calculator_sorting_mode),
|
||||||
|
context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode)
|
||||||
|
).asFlow().map(AttendanceCalculatorSortingMode::getByValue)
|
||||||
|
|
||||||
private val gradeAverageModePref: Preference<GradeAverageMode>
|
private val gradeAverageModePref: Preference<GradeAverageMode>
|
||||||
get() = getObjectFlow(
|
get() = getObjectFlow(
|
||||||
R.string.pref_key_grade_average_mode,
|
R.string.pref_key_grade_average_mode,
|
||||||
|
@ -0,0 +1,103 @@
|
|||||||
|
package io.github.wulkanowy.domain.attendance
|
||||||
|
|
||||||
|
import io.github.wulkanowy.data.*
|
||||||
|
import io.github.wulkanowy.data.db.entities.AttendanceSummary
|
||||||
|
import io.github.wulkanowy.data.db.entities.Semester
|
||||||
|
import io.github.wulkanowy.data.db.entities.Student
|
||||||
|
import io.github.wulkanowy.data.db.entities.Subject
|
||||||
|
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||||
|
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode.*
|
||||||
|
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||||
|
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
|
||||||
|
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||||
|
import io.github.wulkanowy.data.repositories.SubjectRepository
|
||||||
|
import io.github.wulkanowy.utils.allAbsences
|
||||||
|
import io.github.wulkanowy.utils.allPresences
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
class GetAttendanceCalculatorDataUseCase @Inject constructor(
|
||||||
|
private val subjectRepository: SubjectRepository,
|
||||||
|
private val attendanceSummaryRepository: AttendanceSummaryRepository,
|
||||||
|
private val preferencesRepository: PreferencesRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun invoke(
|
||||||
|
student: Student,
|
||||||
|
semester: Semester,
|
||||||
|
forceRefresh: Boolean,
|
||||||
|
): Flow<Resource<List<AttendanceData>>> =
|
||||||
|
subjectRepository.getSubjects(student, semester, forceRefresh)
|
||||||
|
.mapResourceData { subjects -> subjects.sortedBy(Subject::name) }
|
||||||
|
.combineWithResourceData(preferencesRepository.targetAttendanceFlow, ::Pair)
|
||||||
|
.flatMapResourceData { (subjects, targetFreq) ->
|
||||||
|
combineResourceFlows(subjects.map { subject ->
|
||||||
|
attendanceSummaryRepository.getAttendanceSummary(
|
||||||
|
student = student,
|
||||||
|
semester = semester,
|
||||||
|
subjectId = subject.realId,
|
||||||
|
forceRefresh = forceRefresh
|
||||||
|
).mapResourceData { summaries ->
|
||||||
|
summaries.toAttendanceData(subject.name, targetFreq)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Every individual combined flow causes separate network requests to update data.
|
||||||
|
// When there is N child flows, they can cause up to N-1 items to be emitted. Since all
|
||||||
|
// requests are usually completed in less than 5s, there is no need to emit multiple
|
||||||
|
// intermediates that will be visible for barely any time.
|
||||||
|
.debounceIntermediates()
|
||||||
|
}
|
||||||
|
.combineWithResourceData(preferencesRepository.attendanceCalculatorSortingModeFlow, List<AttendanceData>::sortedBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<AttendanceSummary>.toAttendanceData(subjectName: String, targetFreq: Int): AttendanceData {
|
||||||
|
val presences = sumOf { it.allPresences }
|
||||||
|
val absences = sumOf { it.allAbsences }
|
||||||
|
return AttendanceData(
|
||||||
|
subjectName = subjectName,
|
||||||
|
lessonBalance = calcLessonBalance(
|
||||||
|
targetFreq.toDouble() / 100, presences, absences
|
||||||
|
),
|
||||||
|
presences = presences,
|
||||||
|
absences = absences,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calcLessonBalance(targetFreq: Double, presences: Int, absences: Int): Int {
|
||||||
|
val total = presences + absences
|
||||||
|
// The `+ 1` is to avoid false positives in close cases. Eg.:
|
||||||
|
// target frequency 99%, 1 presence. Without the `+ 1` this would be reported shown as
|
||||||
|
// a positive balance of +1, however that is not actually true as skipping one class
|
||||||
|
// would make it so that the balance would actually be negative (-98). The `+ 1`
|
||||||
|
// fixes this and makes sure that in situations like these, it's not reporting incorrect
|
||||||
|
// balances
|
||||||
|
return when {
|
||||||
|
presences / (total + 1f) >= targetFreq -> calcMissingAbsences(
|
||||||
|
targetFreq, absences, presences
|
||||||
|
)
|
||||||
|
presences / (total + 0f) < targetFreq -> -calcMissingPresences(
|
||||||
|
targetFreq, absences, presences
|
||||||
|
)
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calcMissingPresences(targetFreq: Double, absences: Int, presences: Int) =
|
||||||
|
calcMinRequiredPresencesFor(targetFreq, absences) - presences
|
||||||
|
|
||||||
|
private fun calcMinRequiredPresencesFor(targetFreq: Double, absences: Int) =
|
||||||
|
ceil((targetFreq / (1 - targetFreq)) * absences).toInt()
|
||||||
|
|
||||||
|
private fun calcMissingAbsences(targetFreq: Double, absences: Int, presences: Int) =
|
||||||
|
calcMinRequiredAbsencesFor(targetFreq, presences) - absences
|
||||||
|
|
||||||
|
private fun calcMinRequiredAbsencesFor(targetFreq: Double, presences: Int) =
|
||||||
|
floor((presences * (1 - targetFreq)) / targetFreq).toInt()
|
||||||
|
|
||||||
|
private fun List<AttendanceData>.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) {
|
||||||
|
ALPHABETIC -> sortedBy(AttendanceData::subjectName)
|
||||||
|
ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage)
|
||||||
|
LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance)
|
||||||
|
}
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
|
|||||||
import io.github.wulkanowy.databinding.DialogExcuseBinding
|
import io.github.wulkanowy.databinding.DialogExcuseBinding
|
||||||
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
|
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
|
||||||
import io.github.wulkanowy.ui.base.BaseFragment
|
import io.github.wulkanowy.ui.base.BaseFragment
|
||||||
|
import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment
|
||||||
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
|
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
|
||||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||||
import io.github.wulkanowy.ui.modules.main.MainView
|
import io.github.wulkanowy.ui.modules.main.MainView
|
||||||
@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
|
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
|
||||||
|
else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected()
|
||||||
else false
|
else false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
|||||||
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
|
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun openCalculatorView() {
|
||||||
|
(activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance())
|
||||||
|
}
|
||||||
|
|
||||||
override fun startActionMode() {
|
override fun startActionMode() {
|
||||||
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
|
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,36 @@
|
|||||||
package io.github.wulkanowy.ui.modules.attendance
|
package io.github.wulkanowy.ui.modules.attendance
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import io.github.wulkanowy.data.*
|
import io.github.wulkanowy.data.Resource
|
||||||
import io.github.wulkanowy.data.db.entities.Attendance
|
import io.github.wulkanowy.data.db.entities.Attendance
|
||||||
import io.github.wulkanowy.data.db.entities.Semester
|
import io.github.wulkanowy.data.db.entities.Semester
|
||||||
|
import io.github.wulkanowy.data.flatResourceFlow
|
||||||
|
import io.github.wulkanowy.data.logResourceStatus
|
||||||
|
import io.github.wulkanowy.data.mapResourceData
|
||||||
|
import io.github.wulkanowy.data.onResourceData
|
||||||
|
import io.github.wulkanowy.data.onResourceError
|
||||||
|
import io.github.wulkanowy.data.onResourceIntermediate
|
||||||
|
import io.github.wulkanowy.data.onResourceLoading
|
||||||
|
import io.github.wulkanowy.data.onResourceNotLoading
|
||||||
|
import io.github.wulkanowy.data.onResourceSuccess
|
||||||
import io.github.wulkanowy.data.repositories.AttendanceRepository
|
import io.github.wulkanowy.data.repositories.AttendanceRepository
|
||||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||||
|
import io.github.wulkanowy.data.resourceFlow
|
||||||
import io.github.wulkanowy.ui.base.BasePresenter
|
import io.github.wulkanowy.ui.base.BasePresenter
|
||||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||||
import io.github.wulkanowy.utils.*
|
import io.github.wulkanowy.utils.AnalyticsHelper
|
||||||
|
import io.github.wulkanowy.utils.capitalise
|
||||||
|
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
|
||||||
|
import io.github.wulkanowy.utils.isExcusableOrNotExcused
|
||||||
|
import io.github.wulkanowy.utils.isHolidays
|
||||||
|
import io.github.wulkanowy.utils.monday
|
||||||
|
import io.github.wulkanowy.utils.nextSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.previousOrSameSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.previousSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.sunday
|
||||||
|
import io.github.wulkanowy.utils.toFormattedString
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -195,6 +215,11 @@ class AttendancePresenter @Inject constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onCalculatorSwitchSelected(): Boolean {
|
||||||
|
view?.openCalculatorView()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadData(forceRefresh: Boolean = false) {
|
private fun loadData(forceRefresh: Boolean = false) {
|
||||||
Timber.i("Loading attendance data started")
|
Timber.i("Loading attendance data started")
|
||||||
|
|
||||||
|
@ -56,6 +56,8 @@ interface AttendanceView : BaseView {
|
|||||||
|
|
||||||
fun openSummaryView()
|
fun openSummaryView()
|
||||||
|
|
||||||
|
fun openCalculatorView()
|
||||||
|
|
||||||
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
|
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
|
||||||
|
|
||||||
fun startActionMode()
|
fun startActionMode()
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.github.wulkanowy.R
|
||||||
|
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||||
|
import io.github.wulkanowy.databinding.ItemAttendanceCalculatorHeaderBinding
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class AttendanceCalculatorAdapter @Inject constructor() :
|
||||||
|
RecyclerView.Adapter<AttendanceCalculatorAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
var items = emptyList<AttendanceData>()
|
||||||
|
|
||||||
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup, viewType: Int
|
||||||
|
) = ViewHolder(
|
||||||
|
ItemAttendanceCalculatorHeaderBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onBindViewHolder(parent: ViewHolder, position: Int) {
|
||||||
|
with(parent.binding) {
|
||||||
|
val item = items[position]
|
||||||
|
attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}"
|
||||||
|
|
||||||
|
if (item.lessonBalance > 0) {
|
||||||
|
attendanceCalculatorSummaryBalance.text = root.context.getString(
|
||||||
|
R.string.attendance_calculator_summary_balance_positive,
|
||||||
|
item.lessonBalance
|
||||||
|
)
|
||||||
|
} else if (item.lessonBalance < 0) {
|
||||||
|
attendanceCalculatorSummaryBalance.text = root.context.getString(
|
||||||
|
R.string.attendance_calculator_summary_balance_negative,
|
||||||
|
abs(item.lessonBalance)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
attendanceCalculatorSummaryBalance.text = root.context.getString(
|
||||||
|
R.string.attendance_calculator_summary_balance_neutral,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
attendanceCalculatorWarning.isVisible = item.lessonBalance < 0
|
||||||
|
attendanceCalculatorTitle.text = item.subjectName
|
||||||
|
attendanceCalculatorSummaryValues.text = root.context.getString(
|
||||||
|
R.string.attendance_calculator_summary_values,
|
||||||
|
item.presences,
|
||||||
|
item.total
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(val binding: ItemAttendanceCalculatorHeaderBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import io.github.wulkanowy.R
|
||||||
|
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||||
|
import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
|
||||||
|
import io.github.wulkanowy.ui.base.BaseFragment
|
||||||
|
import io.github.wulkanowy.ui.modules.main.MainView
|
||||||
|
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||||
|
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AttendanceCalculatorFragment :
|
||||||
|
BaseFragment<FragmentAttendanceCalculatorBinding>(R.layout.fragment_attendance_calculator),
|
||||||
|
AttendanceCalculatorView, MainView.TitledView {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var presenter: AttendanceCalculatorPresenter
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var attendanceCalculatorAdapter: AttendanceCalculatorAdapter
|
||||||
|
|
||||||
|
override val titleStringId get() = R.string.attendance_title
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = AttendanceCalculatorFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding = FragmentAttendanceCalculatorBinding.bind(view)
|
||||||
|
messageContainer = binding.attendanceCalculatorRecycler
|
||||||
|
presenter.onAttachView(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initView() {
|
||||||
|
with(binding.attendanceCalculatorRecycler) {
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = attendanceCalculatorAdapter
|
||||||
|
addItemDecoration(DividerItemDecoration(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
with(binding) {
|
||||||
|
attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
|
||||||
|
attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
|
||||||
|
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
|
||||||
|
attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||||
|
attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateData(data: List<AttendanceData>) {
|
||||||
|
with(attendanceCalculatorAdapter) {
|
||||||
|
items = data
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearView() {
|
||||||
|
with(attendanceCalculatorAdapter) {
|
||||||
|
items = emptyList()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showEmpty(show: Boolean) {
|
||||||
|
binding.attendanceCalculatorEmpty.isVisible = show
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showErrorView(show: Boolean) {
|
||||||
|
binding.attendanceCalculatorError.isVisible = show
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setErrorDetails(message: String) {
|
||||||
|
binding.attendanceCalculatorErrorMessage.text = message
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showProgress(show: Boolean) {
|
||||||
|
binding.attendanceCalculatorProgress.isVisible = show
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enableSwipe(enable: Boolean) {
|
||||||
|
binding.attendanceCalculatorSwipe.isEnabled = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showContent(show: Boolean) {
|
||||||
|
binding.attendanceCalculatorRecycler.isVisible = show
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showRefresh(show: Boolean) {
|
||||||
|
binding.attendanceCalculatorSwipe.isRefreshing = show
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
presenter.onDetachView()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||||
|
|
||||||
|
import io.github.wulkanowy.data.*
|
||||||
|
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||||
|
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||||
|
import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
|
||||||
|
import io.github.wulkanowy.ui.base.BasePresenter
|
||||||
|
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class AttendanceCalculatorPresenter @Inject constructor(
|
||||||
|
errorHandler: ErrorHandler,
|
||||||
|
studentRepository: StudentRepository,
|
||||||
|
private val semesterRepository: SemesterRepository,
|
||||||
|
private val getAttendanceCalculatorData: GetAttendanceCalculatorDataUseCase,
|
||||||
|
) : BasePresenter<AttendanceCalculatorView>(errorHandler, studentRepository) {
|
||||||
|
|
||||||
|
private lateinit var lastError: Throwable
|
||||||
|
|
||||||
|
override fun onAttachView(view: AttendanceCalculatorView) {
|
||||||
|
super.onAttachView(view)
|
||||||
|
view.initView()
|
||||||
|
Timber.i("Attendance calculator view was initialized")
|
||||||
|
errorHandler.showErrorMessage = ::showErrorViewOnError
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSwipeRefresh() {
|
||||||
|
Timber.i("Force refreshing the attendance calculator")
|
||||||
|
loadData(forceRefresh = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRetry() {
|
||||||
|
view?.run {
|
||||||
|
showErrorView(false)
|
||||||
|
showProgress(true)
|
||||||
|
}
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDetailsClick() {
|
||||||
|
view?.showErrorDetailsDialog(lastError)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadData(forceRefresh: Boolean = false) {
|
||||||
|
flatResourceFlow {
|
||||||
|
val student = studentRepository.getCurrentStudent()
|
||||||
|
val semester = semesterRepository.getCurrentSemester(student)
|
||||||
|
getAttendanceCalculatorData(student, semester, forceRefresh)
|
||||||
|
}
|
||||||
|
.logResourceStatus("load attendance calculator")
|
||||||
|
.onResourceData {
|
||||||
|
view?.run {
|
||||||
|
showProgress(false)
|
||||||
|
showErrorView(false)
|
||||||
|
showContent(it.isNotEmpty())
|
||||||
|
showEmpty(it.isEmpty())
|
||||||
|
updateData(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onResourceIntermediate { view?.showRefresh(true) }
|
||||||
|
.onResourceNotLoading {
|
||||||
|
view?.run {
|
||||||
|
enableSwipe(true)
|
||||||
|
showRefresh(false)
|
||||||
|
showProgress(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onResourceError(errorHandler::dispatch)
|
||||||
|
.launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showErrorViewOnError(message: String, error: Throwable) {
|
||||||
|
view?.run {
|
||||||
|
if (isViewEmpty) {
|
||||||
|
lastError = error
|
||||||
|
setErrorDetails(message)
|
||||||
|
showErrorView(true)
|
||||||
|
showEmpty(false)
|
||||||
|
} else showError(message, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||||
|
|
||||||
|
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||||
|
import io.github.wulkanowy.ui.base.BaseView
|
||||||
|
|
||||||
|
interface AttendanceCalculatorView : BaseView {
|
||||||
|
|
||||||
|
val isViewEmpty: Boolean
|
||||||
|
|
||||||
|
fun initView()
|
||||||
|
|
||||||
|
fun showRefresh(show: Boolean)
|
||||||
|
|
||||||
|
fun showContent(show: Boolean)
|
||||||
|
|
||||||
|
fun showProgress(show: Boolean)
|
||||||
|
|
||||||
|
fun enableSwipe(enable: Boolean)
|
||||||
|
|
||||||
|
fun showEmpty(show: Boolean)
|
||||||
|
|
||||||
|
fun showErrorView(show: Boolean)
|
||||||
|
|
||||||
|
fun setErrorDetails(message: String)
|
||||||
|
|
||||||
|
fun updateData(data: List<AttendanceData>)
|
||||||
|
|
||||||
|
fun clearView()
|
||||||
|
}
|
@ -4,6 +4,7 @@ import android.content.SharedPreferences
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.SeekBarPreference
|
||||||
import com.yariksoffice.lingver.Lingver
|
import com.yariksoffice.lingver.Lingver
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
@ -36,6 +37,15 @@ class AppearanceFragment : PreferenceFragmentCompat(),
|
|||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
|
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
|
||||||
|
val attendanceTargetPref =
|
||||||
|
findPreference<SeekBarPreference>(requireContext().getString(R.string.pref_key_attendance_target))!!
|
||||||
|
attendanceTargetPref.setOnPreferenceChangeListener { _, newValueObj ->
|
||||||
|
val newValue = (((newValueObj as Int).toDouble() + 2.5) / 5).toInt() * 5
|
||||||
|
attendanceTargetPref.value =
|
||||||
|
newValue.coerceIn(attendanceTargetPref.min, attendanceTargetPref.max)
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
|
@ -10,19 +10,19 @@ import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory
|
|||||||
* (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf)
|
* (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private inline val AttendanceSummary.allPresences: Double
|
inline val AttendanceSummary.allPresences: Int
|
||||||
get() = presence.toDouble() + absenceForSchoolReasons + lateness + latenessExcused
|
get() = presence + absenceForSchoolReasons + lateness + latenessExcused
|
||||||
|
|
||||||
private inline val AttendanceSummary.allAbsences: Double
|
inline val AttendanceSummary.allAbsences: Int
|
||||||
get() = absence.toDouble() + absenceExcused
|
get() = absence + absenceExcused
|
||||||
|
|
||||||
inline val Attendance.isExcusableOrNotExcused: Boolean
|
inline val Attendance.isExcusableOrNotExcused: Boolean
|
||||||
get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null
|
get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null
|
||||||
|
|
||||||
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences)
|
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences.toDouble(), allAbsences.toDouble())
|
||||||
|
|
||||||
fun List<AttendanceSummary>.calculatePercentage(): Double {
|
fun List<AttendanceSummary>.calculatePercentage(): Double {
|
||||||
return calculatePercentage(sumOf { it.allPresences }, sumOf { it.allAbsences })
|
return calculatePercentage(sumOf { it.allPresences.toDouble() }, sumOf { it.allAbsences.toDouble() })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculatePercentage(presence: Double, absence: Double): Double {
|
private fun calculatePercentage(presence: Double, absence: Double): Double {
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19,7H9C7.9,7 7,7.9 7,9v10c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V9C21,7.9 20.1,7 19,7zM19,9v2H9V9H19zM13,15v-2h2v2H13zM15,17v2h-2v-2H15zM11,15H9v-2h2V15zM17,13h2v2h-2V13zM9,17h2v2H9V17zM17,19v-2h2v2H17zM6,17H5c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v1h-2V5H5v10h1V17z"/>
|
||||||
|
</vector>
|
103
app/src/main/res/layout/fragment_attendance_calculator.xml
Normal file
103
app/src/main/res/layout/fragment_attendance_calculator.xml
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.modules.attendance.calculator.AttendanceCalculatorFragment">
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/attendanceCalculatorSwipe"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/attendanceCalculatorRecycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:listitem="@layout/item_attendance_calculator_header" />
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
|
android:id="@+id/attendanceCalculatorProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/attendanceCalculatorEmpty"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="invisible"
|
||||||
|
tools:ignore="UseCompoundDrawables">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_main_attendance"
|
||||||
|
app:tint="?colorOnBackground"
|
||||||
|
tools:ignore="contentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/attendance_no_items"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/attendanceCalculatorError"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="invisible"
|
||||||
|
tools:ignore="UseCompoundDrawables"
|
||||||
|
tools:visibility="invisible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_error"
|
||||||
|
app:tint="?colorOnBackground"
|
||||||
|
tools:ignore="contentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/attendanceCalculatorErrorMessage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:text="@string/error_unknown"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/attendanceCalculatorErrorDetails"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:text="@string/all_details" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/attendanceCalculatorErrorRetry"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/all_retry" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
111
app/src/main/res/layout/item_attendance_calculator_header.xml
Normal file
111
app/src/main/res/layout/item_attendance_calculator_header.xml
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:paddingBottom="6dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/attendanceCalculatorPercentage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="right|center_vertical"
|
||||||
|
android:minWidth="32dp"
|
||||||
|
android:minHeight="36dp"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="50" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/attendanceCalculatorPercentagePercentSign"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="%"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@+id/attendanceCalculatorPercentage"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorPercentage" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/attendanceCalculatorSummaryValues"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="left"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorSummaryDot"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/attendanceCalculatorTitle"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle"
|
||||||
|
tools:text="11/123 obecności" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/attendanceCalculatorSummaryDot"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="4dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="·"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorSummaryBalance"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorSummaryValues"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/attendanceCalculatorSummaryBalance"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="left"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:minWidth="24dp"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorWarning"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorSummaryDot"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle"
|
||||||
|
tools:text="12 powyżej celu" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/attendanceCalculatorTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorWarning"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorPercentagePercentSign"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Informatyka" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/attendanceCalculatorWarning"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription=""
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:gravity="center_vertical|right"
|
||||||
|
android:minWidth="24dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_all_round_mark"
|
||||||
|
app:tint="?colorPrimary"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
140
app/src/main/res/layout/pref_target_attendance.xml
Normal file
140
app/src/main/res/layout/pref_target_attendance.xml
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Taken from https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-camerax-release/preference/preference/res/layout/preference_widget_seekbar_material.xml.
|
||||||
|
|
||||||
|
~ Copyright (C) 2018 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<!-- Layout used by SeekBarPreference for the seekbar widget style. -->
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:baselineAligned="false">
|
||||||
|
<include layout="@layout/image_frame"/>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
<TextView
|
||||||
|
android:id="@android:id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
|
android:ellipsize="marquee"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@android:id/summary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@android:id/title"
|
||||||
|
android:layout_alignLeft="@android:id/title"
|
||||||
|
android:layout_alignStart="@android:id/title"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:maxLines="4"
|
||||||
|
style="@style/PreferenceSummaryTextStyle"/>
|
||||||
|
</RelativeLayout>
|
||||||
|
<!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation
|
||||||
|
to the children of this container layout. Otherwise, the animated pressed state will also
|
||||||
|
play for the thumb in the AbsSeekBar in addition to the preference's ripple background.
|
||||||
|
The background of the SeekBar is also set to null to disable the ripple background -->
|
||||||
|
<androidx.preference.UnPressableLinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingLeft="0dp"
|
||||||
|
android:paddingStart="0dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
<!-- The total height of the Seekbar widget's area should be 48dp - this allows for an
|
||||||
|
increased touch area so you do not need to exactly tap the thumb to move it. However,
|
||||||
|
setting the Seekbar height directly causes the thumb and seekbar to be misaligned on
|
||||||
|
API 22 and 23 - so instead we just set 15dp padding above and below, to account for the
|
||||||
|
18dp default height of the Seekbar thumb for a total of 48dp.
|
||||||
|
Note: we set 0dp padding at the start and end of this seekbar to allow it to properly
|
||||||
|
fit into the layout, but this means that there's no leeway on either side for touch
|
||||||
|
input - this might be something we should reconsider down the line. -->
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/seekbar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingLeft="@dimen/preference_seekbar_padding_horizontal"
|
||||||
|
android:paddingStart="@dimen/preference_seekbar_padding_horizontal"
|
||||||
|
android:paddingRight="@dimen/preference_seekbar_padding_horizontal"
|
||||||
|
android:paddingEnd="@dimen/preference_seekbar_padding_horizontal"
|
||||||
|
android:paddingTop="@dimen/preference_seekbar_padding_vertical"
|
||||||
|
android:paddingBottom="@dimen/preference_seekbar_padding_vertical"
|
||||||
|
|
||||||
|
android:background="@null"/>
|
||||||
|
<!-- If the value is shown, we reserve a minimum width of 36dp to allow for consistent
|
||||||
|
seekbar width for smaller values. If the value is ~4 or more digits, it will expand
|
||||||
|
into the seekbar width. -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/seekbar_value"
|
||||||
|
android:minWidth="@dimen/preference_seekbar_value_minWidth"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingRight="0dp"
|
||||||
|
android:paddingEnd="0dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="right"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:fadingEdge="horizontal"
|
||||||
|
android:scrollbars="none"/>
|
||||||
|
<!-- Wulkanowy start -->
|
||||||
|
<TextView
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:paddingLeft="0dp"
|
||||||
|
android:paddingStart="0dp"
|
||||||
|
android:paddingRight="0dp"
|
||||||
|
android:paddingEnd="0dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="left"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:fadingEdge="horizontal"
|
||||||
|
android:scrollbars="none"
|
||||||
|
android:text="%"
|
||||||
|
/>
|
||||||
|
<!-- Wulkanowy end -->
|
||||||
|
</androidx.preference.UnPressableLinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
@ -1,6 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu 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">
|
||||||
|
<item
|
||||||
|
android:id="@+id/attendanceMenuCalculator"
|
||||||
|
android:icon="@drawable/ic_menu_attendance_calculator"
|
||||||
|
android:orderInCategory="0"
|
||||||
|
android:title="@string/attendance_calculator_button"
|
||||||
|
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/attendanceMenuSummary"
|
android:id="@+id/attendanceMenuSummary"
|
||||||
android:icon="@drawable/ic_menu_attendance_summary"
|
android:icon="@drawable/ic_menu_attendance_summary"
|
||||||
|
@ -876,4 +876,7 @@
|
|||||||
<string name="message_unmute">Wyłącz wyciszenie</string>
|
<string name="message_unmute">Wyłącz wyciszenie</string>
|
||||||
<string name="message_mute_success">Wyciszyleś tego użytkownika</string>
|
<string name="message_mute_success">Wyciszyleś tego użytkownika</string>
|
||||||
<string name="message_unmute_success">Wyłączyłeś wyciszenie tego użytkownika</string>
|
<string name="message_unmute_success">Wyłączyłeś wyciszenie tego użytkownika</string>
|
||||||
|
<!-- attendance calculator -->
|
||||||
|
<string name="pref_attendance_target">Docelowa frekwencja (w %)</string>
|
||||||
|
<string name="attendance_calculator_button">Kalkulator frekwencji</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
<string name="pref_default_startup">0</string>
|
<string name="pref_default_startup">0</string>
|
||||||
<bool name="pref_default_attendance_present">true</bool>
|
<bool name="pref_default_attendance_present">true</bool>
|
||||||
|
<integer name="pref_default_attendance_target">50</integer>
|
||||||
|
<string name="pref_default_attendance_calculator_sorting_mode">alphabetic</string>
|
||||||
<string name="pref_default_grade_average_mode">only_one_semester</string>
|
<string name="pref_default_grade_average_mode">only_one_semester</string>
|
||||||
<bool name="pref_default_grade_average_force_calc">false</bool>
|
<bool name="pref_default_grade_average_force_calc">false</bool>
|
||||||
<string name="pref_default_expand_grade_mode">one</string>
|
<string name="pref_default_expand_grade_mode">one</string>
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
<string name="pref_key_start_menu">default_menu_index</string>
|
<string name="pref_key_start_menu">default_menu_index</string>
|
||||||
<string name="pref_key_attendance_present">attendance_present</string>
|
<string name="pref_key_attendance_present">attendance_present</string>
|
||||||
|
<string name="pref_key_attendance_target">attendance_target</string>
|
||||||
|
<string name="pref_key_attendance_calculator_sorting_mode">attendance_calculator_sorting_mode</string>
|
||||||
<string name="pref_key_app_theme">app_theme</string>
|
<string name="pref_key_app_theme">app_theme</string>
|
||||||
<string name="pref_key_dashboard_tiles">dashboard_tiles</string>
|
<string name="pref_key_dashboard_tiles">dashboard_tiles</string>
|
||||||
<string name="pref_key_grade_color_scheme">grade_color_scheme</string>
|
<string name="pref_key_grade_color_scheme">grade_color_scheme</string>
|
||||||
|
@ -79,6 +79,17 @@
|
|||||||
<item>0.75</item>
|
<item>0.75</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="attendance_calculator_sorting_mode_entries">
|
||||||
|
<item>Alphabetically</item>
|
||||||
|
<item>By attendance percentage</item>
|
||||||
|
<item>By lesson balance</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="attendance_calculator_sorting_mode_values" translatable="false">
|
||||||
|
<item>alphabetic</item>
|
||||||
|
<item>attendance_percentage</item>
|
||||||
|
<item>lesson_balance</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="grade_sorting_mode_entries">
|
<string-array name="grade_sorting_mode_entries">
|
||||||
<item>Alphabetically</item>
|
<item>Alphabetically</item>
|
||||||
<item>By date</item>
|
<item>By date</item>
|
||||||
|
@ -258,6 +258,11 @@
|
|||||||
|
|
||||||
<!--Attendance-->
|
<!--Attendance-->
|
||||||
<string name="attendance_summary_button">Attendance summary</string>
|
<string name="attendance_summary_button">Attendance summary</string>
|
||||||
|
<string name="attendance_calculator_button">Attendance calculator</string>
|
||||||
|
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> over target</string>
|
||||||
|
<string name="attendance_calculator_summary_balance_neutral">right on target</string>
|
||||||
|
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> under target</string>
|
||||||
|
<string name="attendance_calculator_summary_values">%1$d/%2$d presences</string>
|
||||||
<string name="attendance_absence_school">Absent for school reasons</string>
|
<string name="attendance_absence_school">Absent for school reasons</string>
|
||||||
<string name="attendance_absence_excused">Excused absence</string>
|
<string name="attendance_absence_excused">Excused absence</string>
|
||||||
<string name="attendance_absence_unexcused">Unexcused absence</string>
|
<string name="attendance_absence_unexcused">Unexcused absence</string>
|
||||||
@ -715,6 +720,8 @@
|
|||||||
<string name="pref_view_grade_average_mode">Calculated average options</string>
|
<string name="pref_view_grade_average_mode">Calculated average options</string>
|
||||||
<string name="pref_view_grade_average_force_calc">Force average calculation by app</string>
|
<string name="pref_view_grade_average_force_calc">Force average calculation by app</string>
|
||||||
<string name="pref_view_present">Show presence</string>
|
<string name="pref_view_present">Show presence</string>
|
||||||
|
<string name="pref_attendance_target">Attendance target</string>
|
||||||
|
<string name="pref_view_attendance_calculator_sorting_mode">Attendance calculator sorting</string>
|
||||||
<string name="pref_view_app_theme">Theme</string>
|
<string name="pref_view_app_theme">Theme</string>
|
||||||
<string name="pref_view_expand_grade">Grades expanding</string>
|
<string name="pref_view_expand_grade">Grades expanding</string>
|
||||||
<string name="pref_view_timetable_show_groups">Show groups next to subjects</string>
|
<string name="pref_view_timetable_show_groups">Show groups next to subjects</string>
|
||||||
|
@ -85,6 +85,25 @@
|
|||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:key="@string/pref_key_attendance_present"
|
app:key="@string/pref_key_attendance_present"
|
||||||
app:title="@string/pref_view_present" />
|
app:title="@string/pref_view_present" />
|
||||||
|
<SeekBarPreference
|
||||||
|
app:defaultValue="@integer/pref_default_attendance_target"
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
android:layout="@layout/pref_target_attendance"
|
||||||
|
app:key="@string/pref_key_attendance_target"
|
||||||
|
app:title="@string/pref_attendance_target"
|
||||||
|
app:min="1"
|
||||||
|
app:updatesContinuously="true"
|
||||||
|
android:max="99"
|
||||||
|
app:showSeekBarValue="true"
|
||||||
|
/>
|
||||||
|
<ListPreference
|
||||||
|
app:defaultValue="@string/pref_default_attendance_calculator_sorting_mode"
|
||||||
|
app:entries="@array/attendance_calculator_sorting_mode_entries"
|
||||||
|
app:entryValues="@array/attendance_calculator_sorting_mode_values"
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:key="@string/pref_key_attendance_calculator_sorting_mode"
|
||||||
|
app:title="@string/pref_view_attendance_calculator_sorting_mode"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user