[UI] Add attendance summary page. Disable presence notifications.

This commit is contained in:
Kuba Szczodrzyński 2020-05-05 22:57:24 +02:00
parent 97412a3736
commit e068f1944f
21 changed files with 500 additions and 148 deletions

View File

@ -96,8 +96,8 @@ class EdudziennikWebAttendance(override val data: DataEdudziennik,
profileId, profileId,
Metadata.TYPE_ATTENDANCE, Metadata.TYPE_ATTENDANCE,
id, id,
profile.empty, profile.empty || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN,
profile.empty profile.empty || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN
)) ))
} }
} }

View File

@ -148,8 +148,8 @@ class IdziennikWebAttendance(override val data: DataIdziennik,
profileId, profileId,
Metadata.TYPE_ATTENDANCE, Metadata.TYPE_ATTENDANCE,
attendanceObject.id, attendanceObject.id,
profile?.empty ?: false, profile?.empty ?: false || baseType == TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN,
profile?.empty ?: false profile?.empty ?: false || baseType == TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN
)) ))
} }
} }

View File

@ -77,8 +77,8 @@ class LibrusApiAttendances(override val data: DataLibrus,
profileId, profileId,
Metadata.TYPE_ATTENDANCE, Metadata.TYPE_ATTENDANCE,
id, id,
profile?.empty ?: false, profile?.empty ?: false || type?.baseType == Attendance.TYPE_PRESENT_CUSTOM || type?.baseType == Attendance.TYPE_UNKNOWN,
profile?.empty ?: false profile?.empty ?: false || type?.baseType == Attendance.TYPE_PRESENT_CUSTOM || type?.baseType == Attendance.TYPE_UNKNOWN
)) ))
} }
} }

View File

@ -72,8 +72,8 @@ class MobidziennikApiAttendance(val data: DataMobidziennik, rows: List<String>)
data.profileId, data.profileId,
Metadata.TYPE_ATTENDANCE, Metadata.TYPE_ATTENDANCE,
id, id,
data.profile?.empty ?: false, data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN,
data.profile?.empty ?: false data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN
)) ))
} }
} }

View File

@ -175,8 +175,8 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
data.profileId, data.profileId,
Metadata.TYPE_ATTENDANCE, Metadata.TYPE_ATTENDANCE,
id, id,
data.profile?.empty ?: false, data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN,
data.profile?.empty ?: false data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN
)) ))
} }
} }

View File

@ -73,8 +73,8 @@ class VulcanApiAttendance(override val data: DataVulcan,
profileId, profileId,
Metadata.TYPE_ATTENDANCE, Metadata.TYPE_ATTENDANCE,
attendanceObject.id, attendanceObject.id,
profile.empty, profile.empty || type.baseType == Attendance.TYPE_PRESENT_CUSTOM || type.baseType == Attendance.TYPE_UNKNOWN,
profile.empty profile.empty || type.baseType == Attendance.TYPE_PRESENT_CUSTOM || type.baseType == Attendance.TYPE_UNKNOWN
)) ))
} }
} }

View File

@ -68,4 +68,9 @@ open class Attendance(
@Ignore @Ignore
var showAsUnseen: Boolean? = null var showAsUnseen: Boolean? = null
@delegate:Ignore
val typeObject by lazy {
AttendanceType(profileId, baseType.toLong(), baseType, typeName, typeShort, typeSymbol, typeColor)
}
} }

View File

@ -21,4 +21,35 @@ data class AttendanceType (
val typeSymbol: String, val typeSymbol: String,
/** A color that the e-register would display, null falls back to app's default */ /** A color that the e-register would display, null falls back to app's default */
val typeColor: Int? val typeColor: Int?
) ) : Comparable<AttendanceType> {
// attendance bar order:
// day_free, present, present_custom, unknown, belated_excused, belated, released, absent_excused, absent,
override fun compareTo(other: AttendanceType): Int {
val type1 = when (baseType) {
Attendance.TYPE_DAY_FREE -> 0
Attendance.TYPE_PRESENT -> 1
Attendance.TYPE_PRESENT_CUSTOM -> 2
Attendance.TYPE_UNKNOWN -> 3
Attendance.TYPE_BELATED_EXCUSED -> 4
Attendance.TYPE_BELATED -> 5
Attendance.TYPE_RELEASED -> 6
Attendance.TYPE_ABSENT_EXCUSED -> 7
Attendance.TYPE_ABSENT -> 8
else -> 9
}
val type2 = when (other.baseType) {
Attendance.TYPE_DAY_FREE -> 0
Attendance.TYPE_PRESENT -> 1
Attendance.TYPE_PRESENT_CUSTOM -> 2
Attendance.TYPE_UNKNOWN -> 3
Attendance.TYPE_BELATED_EXCUSED -> 4
Attendance.TYPE_BELATED -> 5
Attendance.TYPE_RELEASED -> 6
Attendance.TYPE_ABSENT_EXCUSED -> 7
Attendance.TYPE_ABSENT -> 8
else -> 9
}
return type1 - type2
}
}

View File

@ -24,5 +24,6 @@ class AttendanceFull(
// metadata // metadata
var seen = false var seen = false
get() = field || baseType == TYPE_PRESENT
var notified = false var notified = false
} }

View File

@ -93,7 +93,7 @@ class AttendanceBar : View {
val textBounds = Rect() val textBounds = Rect()
textPaint.getTextBounds(e.count.toString(), 0, e.count.toString().length, textBounds) textPaint.getTextBounds(e.count.toString(), 0, e.count.toString().length, textBounds)
if (width > textBounds.width() + 8.dp) { if (width > textBounds.width() + 8.dp && height > textBounds.height() + 2.dp) {
textPaint.color = Colors.legibleTextColor(e.color) textPaint.color = Colors.legibleTextColor(e.color)
canvas.drawText(e.count.toString(), left + width / 2, bottom - height / 2 + textBounds.height()/2, textPaint) canvas.drawText(e.count.toString(), left + width / 2, bottom - height / 2 + textBounds.height()/2, textPaint)
} }

View File

@ -49,7 +49,6 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope {
context ?: return null context ?: return null
app = activity.application as App app = activity.application as App
b = AttendanceListFragmentBinding.inflate(inflater) b = AttendanceListFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root return b.root
} }
@ -67,7 +66,7 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope {
// load & configure the adapter // load & configure the adapter
adapter.items = withContext(Dispatchers.Default) { processAttendance(items) } adapter.items = withContext(Dispatchers.Default) { processAttendance(items) }
if (items.isNotNullNorEmpty() && b.list.adapter == null) { if (adapter.items.isNotNullNorEmpty() && b.list.adapter == null) {
b.list.adapter = adapter b.list.adapter = adapter
b.list.apply { b.list.apply {
setHasFixedSize(true) setHasFixedSize(true)
@ -76,6 +75,7 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope {
} }
} }
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
setSwipeToRefresh(adapter.items.isNullOrEmpty())
if (firstRun) { if (firstRun) {
expandSubject(adapter) expandSubject(adapter)
@ -84,7 +84,7 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope {
// show/hide relevant views // show/hide relevant views
b.progressBar.isVisible = false b.progressBar.isVisible = false
if (items.isNullOrEmpty()) { if (adapter.items.isNullOrEmpty()) {
b.list.isVisible = false b.list.isVisible = false
b.noData.isVisible = true b.noData.isVisible = true
} else { } else {
@ -141,6 +141,8 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope {
items.sortByDescending { it.rangeStart } items.sortByDescending { it.rangeStart }
val iterator = items.listIterator() val iterator = items.listIterator()
if (!iterator.hasNext())
return items.toMutableList()
var element = iterator.next() var element = iterator.next()
while (iterator.hasNext()) { while (iterator.hasNext()) {
var nextElement = iterator.next() var nextElement = iterator.next()
@ -170,14 +172,19 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope {
items.forEach { month -> items.forEach { month ->
month.typeCountMap = month.items month.typeCountMap = month.items
.groupBy { it.baseType } .groupBy { it.typeObject }
.map { it.key to it.value.size } .map { it.key to it.value.size }
.sortedBy { it.first } .sortedBy { it.first }
.toMap() .toMap()
val totalCount = month.typeCountMap.entries.sumBy { it.value } val totalCount = month.typeCountMap.entries.sumBy {
when (it.key.baseType) {
Attendance.TYPE_UNKNOWN -> 0
else -> it.value
}
}
val presenceCount = month.typeCountMap.entries.sumBy { val presenceCount = month.typeCountMap.entries.sumBy {
when (it.key) { when (it.key.baseType) {
Attendance.TYPE_PRESENT, Attendance.TYPE_PRESENT,
Attendance.TYPE_PRESENT_CUSTOM, Attendance.TYPE_PRESENT_CUSTOM,
Attendance.TYPE_BELATED, Attendance.TYPE_BELATED,

View File

@ -4,35 +4,43 @@
package pl.szczodrzynski.edziennik.ui.modules.attendance package pl.szczodrzynski.edziennik.ui.modules.attendance
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.Animation
import android.view.animation.Transformation
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.graphics.ColorUtils
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.* import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.databinding.AttendanceListFragmentBinding import pl.szczodrzynski.edziennik.databinding.AttendanceSummaryFragmentBinding
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.startCoroutineTimer
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceFragment.Companion.VIEW_SUMMARY import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceFragment.Companion.VIEW_SUMMARY
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceSubject import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceSubject
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject
import pl.szczodrzynski.edziennik.utils.models.Date
import java.text.DecimalFormat
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class AttendanceSummaryFragment : LazyFragment(), CoroutineScope { class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
companion object { companion object {
private const val TAG = "AttendanceSummaryFragment" private const val TAG = "AttendanceSummaryFragment"
private var periodSelection = 0
} }
private lateinit var app: App private lateinit var app: App
private lateinit var activity: MainActivity private lateinit var activity: MainActivity
private lateinit var b: AttendanceListFragmentBinding private lateinit var b: AttendanceSummaryFragmentBinding
private val job: Job = Job() private val job: Job = Job()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
@ -41,13 +49,13 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
// local/private variables go here // local/private variables go here
private val manager by lazy { app.attendanceManager } private val manager by lazy { app.attendanceManager }
private var expandSubjectId = 0L private var expandSubjectId = 0L
private var attendance = listOf<AttendanceFull>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
context ?: return null context ?: return null
app = activity.application as App app = activity.application as App
b = AttendanceListFragmentBinding.inflate(inflater) b = AttendanceSummaryFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root return b.root
} }
@ -63,16 +71,17 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
if (!isAdded) return@launch if (!isAdded) return@launch
// load & configure the adapter // load & configure the adapter
adapter.items = withContext(Dispatchers.Default) { processAttendance(items) } attendance = items
if (items.isNotNullNorEmpty() && b.list.adapter == null) { adapter.items = withContext(Dispatchers.Default) { processAttendance() }
if (adapter.items.isNotNullNorEmpty() && b.list.adapter == null) {
b.list.adapter = adapter b.list.adapter = adapter
b.list.apply { b.list.apply {
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
addOnScrollListener(onScrollListener)
} }
} }
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
setSwipeToRefresh(adapter.items.isNullOrEmpty())
if (firstRun) { if (firstRun) {
expandSubject(adapter) expandSubject(adapter)
@ -81,10 +90,12 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
// show/hide relevant views // show/hide relevant views
b.progressBar.isVisible = false b.progressBar.isVisible = false
if (items.isNullOrEmpty()) { if (adapter.items.isNullOrEmpty()) {
b.statsLayout.isVisible = false
b.list.isVisible = false b.list.isVisible = false
b.noData.isVisible = true b.noData.isVisible = true
} else { } else {
b.statsLayout.isVisible = true
b.list.isVisible = true b.list.isVisible = true
b.noData.isVisible = false b.noData.isVisible = false
} }
@ -93,6 +104,36 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
adapter.onAttendanceClick = { adapter.onAttendanceClick = {
//GradeDetailsDialog(activity, it) //GradeDetailsDialog(activity, it)
} }
b.toggleGroup.check(when (periodSelection) {
0 -> R.id.allYear
1 -> R.id.semester1
2 -> R.id.semester2
else -> R.id.allYear
})
b.toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (!isChecked)
return@addOnButtonCheckedListener
periodSelection = when (checkedId) {
R.id.allYear -> 0
R.id.semester1 -> 1
R.id.semester2 -> 2
else -> 0
}
this@AttendanceSummaryFragment.launch {
adapter.items = withContext(Dispatchers.Default) { processAttendance() }
if (adapter.items.isNullOrEmpty()) {
b.statsLayout.isVisible = false
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.statsLayout.isVisible = true
b.list.isVisible = true
b.noData.isVisible = false
}
adapter.notifyDataSetChanged()
}
}
}; return true} }; return true}
private fun expandSubject(adapter: AttendanceAdapter) { private fun expandSubject(adapter: AttendanceAdapter) {
@ -116,7 +157,14 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
} }
@Suppress("SuspendFunctionOnCoroutineScope") @Suppress("SuspendFunctionOnCoroutineScope")
private fun processAttendance(attendance: List<AttendanceFull>): MutableList<Any> { private fun processAttendance(): MutableList<Any> {
val attendance = when (periodSelection) {
0 -> attendance
1 -> attendance.filter { it.semester == 1 }
2 -> attendance.filter { it.semester == 2 }
else -> attendance
}
if (attendance.isEmpty()) if (attendance.isEmpty())
return mutableListOf() return mutableListOf()
@ -129,16 +177,24 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
) } ) }
.sortedBy { it.subjectName.toLowerCase() } .sortedBy { it.subjectName.toLowerCase() }
var totalCountSum = 0
var presenceCountSum = 0
items.forEach { subject -> items.forEach { subject ->
subject.typeCountMap = subject.items subject.typeCountMap = subject.items
.groupBy { it.baseType } .groupBy { it.typeObject }
.map { it.key to it.value.size } .map { it.key to it.value.size }
.sortedBy { it.first } .sortedBy { it.first }
.toMap() .toMap()
val totalCount = subject.typeCountMap.entries.sumBy { it.value } val totalCount = subject.typeCountMap.entries.sumBy {
when (it.key.baseType) {
Attendance.TYPE_UNKNOWN -> 0
else -> it.value
}
}
val presenceCount = subject.typeCountMap.entries.sumBy { val presenceCount = subject.typeCountMap.entries.sumBy {
when (it.key) { when (it.key.baseType) {
Attendance.TYPE_PRESENT, Attendance.TYPE_PRESENT,
Attendance.TYPE_PRESENT_CUSTOM, Attendance.TYPE_PRESENT_CUSTOM,
Attendance.TYPE_BELATED, Attendance.TYPE_BELATED,
@ -147,6 +203,8 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
else -> 0 else -> 0
} }
} }
totalCountSum += totalCount
presenceCountSum += presenceCount
subject.percentage = if (totalCount == 0) subject.percentage = if (totalCount == 0)
0f 0f
@ -157,6 +215,91 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
subject.items.removeAll { it.baseType == Attendance.TYPE_PRESENT } subject.items.removeAll { it.baseType == Attendance.TYPE_PRESENT }
} }
val typeCountMap = attendance
.groupBy { it.typeObject }
.map { it.key to it.value.size }
.sortedBy { it.first }
.toMap()
val percentage = if (totalCountSum == 0)
0f
else
presenceCountSum.toFloat() / totalCountSum.toFloat() * 100f
launch {
b.attendanceBar.setAttendanceData(typeCountMap.mapKeys { manager.getAttendanceColor(it.key) })
b.attendanceBar.isInvisible = typeCountMap.isEmpty()
b.previewContainer.removeAllViews()
val sum = typeCountMap.entries.sumBy { it.value }.toFloat()
typeCountMap.forEach { (type, count) ->
val layout = LinearLayout(activity)
val attendanceObject = Attendance(
profileId = 0,
id = 0,
baseType = type.baseType,
typeName = "",
typeShort = type.typeShort,
typeSymbol = type.typeSymbol,
typeColor = type.typeColor,
date = Date(0, 0, 0),
startTime = null,
semester = 0,
teacherId = 0,
subjectId = 0,
addedDate = 0
)
layout.addView(AttendanceView(activity, attendanceObject, manager))
layout.addView(TextView(activity).also {
it.setText(R.string.attendance_percentage_format, count/sum*100f)
it.setPadding(0, 0, 5.dp, 0)
})
layout.setPadding(0, 8.dp, 0, 8.dp)
b.previewContainer.addView(layout)
}
if (percentage == 0f) {
b.percentage.isInvisible = true
b.percentageCircle.isInvisible = true
}
else {
b.percentage.isVisible = true
b.percentageCircle.isVisible = true
b.percentage.setText(R.string.attendance_period_summary_format, percentage)
val df = DecimalFormat("0.##")
b.percentageCircle.setProgressTextAdapter { value ->
df.format(value) + "%"
}
b.percentageCircle.maxProgress = 100.0
animatePercentageIndicator(percentage.toDouble())
}
}
return items.toMutableList() return items.toMutableList()
} }
private fun animatePercentageIndicator(targetProgress: Double) {
val startingProgress = b.percentageCircle.progress
val progressChange = targetProgress - startingProgress
val a: Animation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
val progress = startingProgress + (progressChange * interpolatedTime)
//if (interpolatedTime == 1f)
// progress = startingProgress + progressChange
val color = ColorUtils.blendARGB(Color.RED, Color.GREEN, progress.toFloat() / 100.0f)
b.percentageCircle.progressColor = color
b.percentageCircle.setProgress(progress, 100.0)
}
override fun willChangeBounds(): Boolean {
return false
}
}
a.duration = 1300
a.interpolator = AccelerateDecelerateInterpolator()
b.percentageCircle.startAnimation(a)
}
} }

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.ui.modules.attendance.models package pl.szczodrzynski.edziennik.ui.modules.attendance.models
import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.AttendanceType
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
@ -20,6 +21,6 @@ data class AttendanceMonth(
var hasUnseen: Boolean = false var hasUnseen: Boolean = false
get() = field || items.any { it.baseType != Attendance.TYPE_PRESENT && !it.seen } get() = field || items.any { it.baseType != Attendance.TYPE_PRESENT && !it.seen }
var typeCountMap: Map<Int, Int> = mapOf() var typeCountMap: Map<AttendanceType, Int> = mapOf()
var percentage: Float = 0f var percentage: Float = 0f
} }

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.ui.modules.attendance.models package pl.szczodrzynski.edziennik.ui.modules.attendance.models
import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.AttendanceType
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
@ -20,6 +21,6 @@ data class AttendanceSubject(
var hasUnseen: Boolean = false var hasUnseen: Boolean = false
get() = field || items.any { it.baseType != Attendance.TYPE_PRESENT && !it.seen } get() = field || items.any { it.baseType != Attendance.TYPE_PRESENT && !it.seen }
var typeCountMap: Map<Int, Int> = mapOf() var typeCountMap: Map<AttendanceType, Int> = mapOf()
var percentage: Float = 0f var percentage: Float = 0f
} }

View File

@ -63,11 +63,11 @@ class MonthViewHolder(
val attendance = Attendance( val attendance = Attendance(
profileId = 0, profileId = 0,
id = 0, id = 0,
baseType = type, baseType = type.baseType,
typeName = "", typeName = "",
typeShort = manager.getTypeShort(type), typeShort = type.typeShort,
typeSymbol = manager.getTypeShort(type), typeSymbol = type.typeSymbol,
typeColor = manager.getAttendanceColor(type), typeColor = type.typeColor,
date = Date(0, 0, 0), date = Date(0, 0, 0),
startTime = null, startTime = null,
semester = 0, semester = 0,
@ -80,7 +80,7 @@ class MonthViewHolder(
it.setText(R.string.attendance_percentage_format, count/sum*100f) it.setText(R.string.attendance_percentage_format, count/sum*100f)
it.setPadding(0, 0, 5.dp, 0) it.setPadding(0, 0, 5.dp, 0)
}) })
layout.setPadding(0, 8.dp, 0, 0) layout.setPadding(0, 8.dp, 0, 8.dp)
b.previewContainer.addView(layout) b.previewContainer.addView(layout)
} }

View File

@ -6,31 +6,24 @@ package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isInvisible
import androidx.core.view.isVisible 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.Attendance import pl.szczodrzynski.edziennik.databinding.AttendanceItemContainerSubjectBinding
import pl.szczodrzynski.edziennik.databinding.AttendanceItemContainerBarBinding
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.setText import pl.szczodrzynski.edziennik.setText
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter.Companion.STATE_CLOSED import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter.Companion.STATE_CLOSED
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceView
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceSubject import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceSubject
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
class SubjectViewHolder( class SubjectViewHolder(
inflater: LayoutInflater, inflater: LayoutInflater,
parent: ViewGroup, parent: ViewGroup,
val b: AttendanceItemContainerBarBinding = AttendanceItemContainerBarBinding.inflate(inflater, parent, false) val b: AttendanceItemContainerSubjectBinding = AttendanceItemContainerSubjectBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<AttendanceSubject, AttendanceAdapter> { ) : RecyclerView.ViewHolder(b.root), BindableViewHolder<AttendanceSubject, AttendanceAdapter> {
companion object { companion object {
private const val TAG = "SubjectViewHolder" private const val TAG = "SubjectViewHolder"
@ -51,48 +44,14 @@ class SubjectViewHolder(
b.attendanceBar.setAttendanceData(item.typeCountMap.mapKeys { manager.getAttendanceColor(it.key) }) b.attendanceBar.setAttendanceData(item.typeCountMap.mapKeys { manager.getAttendanceColor(it.key) })
b.previewContainer.isInvisible = item.state != STATE_CLOSED b.percentage.isVisible = true
b.summaryContainer.isInvisible = item.state == STATE_CLOSED
b.percentage.isVisible = item.state == STATE_CLOSED
b.previewContainer.removeAllViews()
val sum = item.typeCountMap.entries.sumBy { it.value }.toFloat()
item.typeCountMap.forEach { (type, count) ->
val layout = LinearLayout(contextWrapper)
val attendance = Attendance(
profileId = 0,
id = 0,
baseType = type,
typeName = "",
typeShort = manager.getTypeShort(type),
typeSymbol = manager.getTypeShort(type),
typeColor = manager.getAttendanceColor(type),
date = Date(0, 0, 0),
startTime = null,
semester = 0,
teacherId = 0,
subjectId = 0,
addedDate = 0
)
layout.addView(AttendanceView(contextWrapper, attendance, manager))
layout.addView(TextView(contextWrapper).also {
it.setText(R.string.attendance_percentage_format, count/sum*100f)
it.setPadding(0, 0, 5.dp, 0)
})
layout.setPadding(0, 8.dp, 0, 0)
b.previewContainer.addView(layout)
}
if (item.percentage == 0f) { if (item.percentage == 0f) {
b.percentage.isVisible = false b.percentage.isVisible = false
b.percentage.text = null b.percentage.text = null
b.summaryContainer.isVisible = false
b.summaryContainer.text = null
} }
else { else {
b.percentage.setText(R.string.attendance_percentage_format, item.percentage) b.percentage.setText(R.string.attendance_percentage_format, item.percentage)
b.summaryContainer.setText(R.string.attendance_period_summary_format, item.percentage)
} }
} }
} }

View File

@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.AttendanceType
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.startCoroutineTimer import pl.szczodrzynski.edziennik.startCoroutineTimer
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -49,6 +50,12 @@ class AttendanceManager(val app: App) : CoroutineScope {
else -> 0xff64b5f6.toInt() else -> 0xff64b5f6.toInt()
} }
} }
fun getAttendanceColor(typeObject: AttendanceType): Int {
return (if (useSymbols) typeObject.typeColor else null) ?: when (typeObject.baseType) {
Attendance.TYPE_PRESENT_CUSTOM -> typeObject.typeColor ?: 0xff64b5f6.toInt()
else -> getAttendanceColor(typeObject.baseType)
}
}
fun getAttendanceColor(attendance: Attendance): Int { fun getAttendanceColor(attendance: Attendance): Int {
return (if (useSymbols) attendance.typeColor else null) ?: when (attendance.baseType) { return (if (useSymbols) attendance.typeColor else null) ?: when (attendance.baseType) {
Attendance.TYPE_PRESENT_CUSTOM -> attendance.typeColor ?: 0xff64b5f6.toInt() Attendance.TYPE_PRESENT_CUSTOM -> attendance.typeColor ?: 0xff64b5f6.toInt()

View File

@ -52,8 +52,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="32dp" android:minHeight="32dp"
android:text="@string/attendance_config_show_presence_in_month" android:text="@string/attendance_config_show_presence_in_month" />
android:visibility="gone" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</layout> </layout>

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-5-5.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?selectableItemBackground">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@drawable/divider"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:maxLines="2"
android:textColor="?android:textColorPrimary"
android:textSize="20sp"
tools:text="historia i społeczeństwo" />
<View
android:id="@+id/unread"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:visibility="gone"
android:background="@drawable/unread_red_circle"
tools:visibility="visible"/>
<TextView
android:id="@+id/percentage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
tools:text="6,5%" />
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/dropdownIcon"
android:layout_width="24dp"
android:layout_height="36dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:scaleType="centerInside"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-chevron-down"
app:iiv_size="18dp"
tools:src="@android:drawable/ic_menu_more" />
</LinearLayout>
<pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceBar
android:id="@+id/attendanceBar"
android:layout_width="match_parent"
android:layout_height="12dp"
android:layout_marginHorizontal="8dp"
android:layout_marginBottom="8dp" />
</LinearLayout>
</layout>

View File

@ -7,11 +7,6 @@
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">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -43,5 +38,4 @@
tools:listitem="@layout/attendance_item_attendance" tools:listitem="@layout/attendance_item_attendance"
tools:visibility="visible" /> tools:visibility="visible" />
</FrameLayout> </FrameLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout> </layout>

View File

@ -7,20 +7,143 @@
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">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator <LinearLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout <com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggleGroup"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:layout_margin="8dp"
app:singleSelection="true"
app:selectionRequired="true"
android:gravity="center_horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/semester1"
style="?materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Semestr 1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/semester2"
style="?materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Semestr 2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/allYear"
style="?materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cały rok" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/statsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/percentage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:textAppearance="@style/NavView.TextView.Subtitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Obecność w tym okresie: 99,7%" />
<antonkozyriatskyi.circularprogressindicator.CircularProgressIndicator
android:id="@+id/percentageCircle"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginHorizontal="8dp"
android:layout_marginBottom="16dp"
app:direction="clockwise"
app:drawDot="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:progressBackgroundStrokeWidth="9dp"
app:progressStrokeWidth="10dp"
app:progressCap="butt"
app:textSize="0sp"
android:visibility="invisible"
tools:visibility="visible" />
<pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceBar
android:id="@+id/attendanceBar"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toStartOf="@+id/percentageCircle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/percentage"
android:visibility="invisible"
tools:visibility="visible" />
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/previewContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal"
app:flexWrap="wrap"
app:layout_constraintEnd_toStartOf="@+id/percentageCircle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/attendanceBar">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="8dp">
<pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:background="@drawable/bg_rounded_4dp"
tools:backgroundTint="#43a047"
tools:layout_marginEnd="5dp"
tools:layout_marginRight="5dp"
tools:paddingHorizontal="5dp"
tools:singleLine="true"
tools:text="w"
tools:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:layout_marginEnd="5dp"
tools:layout_marginRight="5dp"
tools:text="6,8%" />
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" /> android:layout_gravity="center"
android:layout_marginVertical="64dp"
tools:visibility="gone" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/noData" android:id="@+id/noData"
@ -38,10 +161,11 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/list" android:id="@+id/list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
tools:listitem="@layout/attendance_item_attendance" tools:listitem="@layout/attendance_item_attendance"
tools:visibility="visible" /> tools:visibility="visible" />
</FrameLayout> </LinearLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator> </androidx.core.widget.NestedScrollView>
</LinearLayout>
</layout> </layout>