[UI] Add new attendance UI module.

This commit is contained in:
Kuba Szczodrzyński 2020-05-04 22:47:27 +02:00
parent 6436a17036
commit 9167d53a1a
38 changed files with 2034 additions and 155 deletions

View File

@ -30,6 +30,7 @@ class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List<ConfigEn
val grades by lazy { ProfileConfigGrades(this) } val grades by lazy { ProfileConfigGrades(this) }
val ui by lazy { ProfileConfigUI(this) } val ui by lazy { ProfileConfigUI(this) }
val sync by lazy { ProfileConfigSync(this) } val sync by lazy { ProfileConfigSync(this) }
val attendance by lazy { ProfileConfigAttendance(this) }
/* /*
val timetable by lazy { ConfigTimetable(this) } val timetable by lazy { ConfigTimetable(this) }
val grades by lazy { ConfigGrades(this) }*/ val grades by lazy { ConfigGrades(this) }*/

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-29.
*/
package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
class ProfileConfigAttendance(private val config: ProfileConfig) {
private var mAttendancePageSelection: Int? = null
var attendancePageSelection: Int
get() { mAttendancePageSelection = mAttendancePageSelection ?: config.values.get("attendancePageSelection", 1); return mAttendancePageSelection ?: 1 }
set(value) { config.set("attendancePageSelection", value); mAttendancePageSelection = value }
private var mUseSymbols: Boolean? = null
var useSymbols: Boolean
get() { mUseSymbols = mUseSymbols ?: config.values.get("useSymbols", false); return mUseSymbols ?: false }
set(value) { config.set("useSymbols", value); mUseSymbols = value }
private var mGroupConsecutiveDays: Boolean? = null
var groupConsecutiveDays: Boolean
get() { mGroupConsecutiveDays = mGroupConsecutiveDays ?: config.values.get("groupConsecutiveDays", true); return mGroupConsecutiveDays ?: true }
set(value) { config.set("groupConsecutiveDays", value); mGroupConsecutiveDays = value }
private var mShowPresenceInMonth: Boolean? = null
var showPresenceInMonth: Boolean
get() { mShowPresenceInMonth = mShowPresenceInMonth ?: config.values.get("showPresenceInMonth", false); return mShowPresenceInMonth ?: false }
set(value) { config.set("showPresenceInMonth", value); mShowPresenceInMonth = value }
}

View File

@ -69,6 +69,6 @@ abstract class AttendanceDao : BaseDao<Attendance, AttendanceFull> {
fun getByIdNow(profileId: Int, id: Long) = fun getByIdNow(profileId: Int, id: Long) =
getOneNow("$QUERY WHERE attendances.profileId = $profileId AND attendanceId = $id") getOneNow("$QUERY WHERE attendances.profileId = $profileId AND attendanceId = $id")
@Query("UPDATE attendances SET keep = 0 WHERE profileId = :profileId AND attendanceDate > :date") @Query("UPDATE attendances SET keep = 0 WHERE profileId = :profileId AND attendanceDate >= :date")
abstract fun dontKeepAfterDate(profileId: Int, date: Date?) abstract fun dontKeepAfterDate(profileId: Int, date: Date?)
} }

View File

@ -116,6 +116,8 @@ interface BaseDao<T : Keepable, F : T> {
if (forceReplace) if (forceReplace)
replaceAll(items) replaceAll(items)
else else
upsertAll(items) upsertAll(items, removeNotKept = false)
if (removeNotKept) removeNotKept()
} }
} }

View File

@ -47,15 +47,18 @@ open class Attendance(
var addedDate: Long = System.currentTimeMillis() var addedDate: Long = System.currentTimeMillis()
) : Keepable() { ) : Keepable() {
companion object { companion object {
const val TYPE_UNKNOWN = -1 const val TYPE_UNKNOWN = -1 // #3f51b5
const val TYPE_PRESENT = 0 const val TYPE_PRESENT = 0 // #009688
const val TYPE_PRESENT_CUSTOM = 10 // count as presence AND show in the list const val TYPE_PRESENT_CUSTOM = 10 // count as presence AND show in the list + custom color, fallback: #3f51b5
const val TYPE_ABSENT = 1 const val TYPE_ABSENT = 1 // #ff3d00
const val TYPE_ABSENT_EXCUSED = 2 const val TYPE_ABSENT_EXCUSED = 2 // #76ff03
const val TYPE_RELEASED = 3 const val TYPE_RELEASED = 3 // #9e9e9e
const val TYPE_BELATED = 4 const val TYPE_BELATED = 4 // #ffc107
const val TYPE_BELATED_EXCUSED = 5 const val TYPE_BELATED_EXCUSED = 5 // #ffc107
const val TYPE_DAY_FREE = 6 const val TYPE_DAY_FREE = 6 // #43a047
// attendance bar order:
// day_free, present, present_custom, unknown, belated_excused, belated, released, absent_excused, absent,
} }
@ColumnInfo(name = "attendanceLessonTopic") @ColumnInfo(name = "attendanceLessonTopic")
@ -64,5 +67,5 @@ open class Attendance(
var lessonNumber: Int? = null var lessonNumber: Int? = null
@Ignore @Ignore
var showAsUnseen = false var showAsUnseen: Boolean? = null
} }

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-5-4.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
import android.annotation.SuppressLint
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.AttendanceConfigDialogBinding
import pl.szczodrzynski.edziennik.onChange
class AttendanceConfigDialog(
val activity: AppCompatActivity,
private val reloadOnDismiss: Boolean = true,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) {
companion object {
const val TAG = "GradesConfigDialog"
}
private val app by lazy { activity.application as App }
private val profileConfig by lazy { app.config.getFor(app.profileId).attendance }
private lateinit var b: AttendanceConfigDialogBinding
private lateinit var dialog: AlertDialog
init { run {
if (activity.isFinishing)
return@run
b = AttendanceConfigDialogBinding.inflate(activity.layoutInflater)
onShowListener?.invoke(TAG)
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.menu_attendance_config)
.setView(b.root)
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
.setOnDismissListener {
saveConfig()
onDismissListener?.invoke(TAG)
if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget()
}
.create()
initView()
loadConfig()
dialog.show()
}}
@SuppressLint("SetTextI18n")
private fun loadConfig() {
b.useSymbols.isChecked = profileConfig.useSymbols
b.groupConsecutiveDays.isChecked = profileConfig.groupConsecutiveDays
b.showPresenceInMonth.isChecked = profileConfig.showPresenceInMonth
}
private fun saveConfig() {
// nothing to do here, yet
}
private fun initView() {
b.useSymbols.onChange { _, isChecked ->
profileConfig.useSymbols = isChecked
}
b.groupConsecutiveDays.onChange { _, isChecked ->
profileConfig.groupConsecutiveDays = isChecked
}
b.showPresenceInMonth.onChange { _, isChecked ->
profileConfig.showPresenceInMonth = isChecked
}
}
}

View File

@ -1,135 +0,0 @@
package pl.szczodrzynski.edziennik.ui.modules.attendance;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_ABSENT;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_ABSENT_EXCUSED;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_BELATED;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_BELATED_EXCUSED;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_DAY_FREE;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_PRESENT;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_RELEASED;
public class AttendanceAdapter extends RecyclerView.Adapter<AttendanceAdapter.ViewHolder> {
private Context context;
public List<AttendanceFull> attendanceList;
//getting the context and product list with constructor
public AttendanceAdapter(Context mCtx, List<AttendanceFull> noticeList) {
this.context = mCtx;
this.attendanceList = noticeList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//inflating and returning our view holder
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.row_attendance_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
App app = (App) context.getApplicationContext();
AttendanceFull attendance = attendanceList.get(position);
holder.attendanceLessonTopic.setText(attendance.getLessonTopic());
holder.attendanceTeacher.setText(attendance.getTeacherName());
holder.attendanceSubject.setText(attendance.getSubjectLongName());
holder.attendanceDate.setText(attendance.getDate().getStringDmy());
holder.attendanceTime.setText(attendance.getStartTime().getStringHM());
switch (attendance.getBaseType()) {
case TYPE_DAY_FREE:
holder.attendanceType.getBackground().setColorFilter(new PorterDuffColorFilter(0xff166ee0, PorterDuff.Mode.MULTIPLY));
holder.attendanceType.setText(R.string.attendance_free_day);
break;
case TYPE_ABSENT:
holder.attendanceType.getBackground().setColorFilter(new PorterDuffColorFilter(0xfff44336, PorterDuff.Mode.MULTIPLY));
holder.attendanceType.setText(R.string.attendance_absent);
break;
case TYPE_ABSENT_EXCUSED:
holder.attendanceType.getBackground().setColorFilter(new PorterDuffColorFilter(0xffaeea00, PorterDuff.Mode.MULTIPLY));
holder.attendanceType.setText(R.string.attendance_absent_excused);
break;
case TYPE_BELATED:
holder.attendanceType.getBackground().setColorFilter(new PorterDuffColorFilter(0xffffca28, PorterDuff.Mode.MULTIPLY));
holder.attendanceType.setText(R.string.attendance_belated);
break;
case TYPE_BELATED_EXCUSED:
holder.attendanceType.getBackground().setColorFilter(new PorterDuffColorFilter(0xff4bb733, PorterDuff.Mode.MULTIPLY));
holder.attendanceType.setText(R.string.attendance_belated_excused);
break;
case TYPE_RELEASED:
holder.attendanceType.getBackground().setColorFilter(new PorterDuffColorFilter(0xff9e9e9e, PorterDuff.Mode.MULTIPLY));
holder.attendanceType.setText(R.string.attendance_released);
break;
case TYPE_PRESENT:
holder.attendanceType.getBackground().setColorFilter(new PorterDuffColorFilter(0xffffae00, PorterDuff.Mode.MULTIPLY));
holder.attendanceType.setText(R.string.attendance_present);
break;
default:
holder.attendanceType.getBackground().setColorFilter(new PorterDuffColorFilter(0xff03a9f4, PorterDuff.Mode.MULTIPLY));
holder.attendanceType.setText("?");
break;
}
holder.attendanceType.setText(attendance.getTypeShort());
if (!attendance.getSeen()) {
holder.attendanceLessonTopic.setBackground(context.getResources().getDrawable(R.drawable.bg_rounded_8dp));
holder.attendanceLessonTopic.getBackground().setColorFilter(new PorterDuffColorFilter(0x692196f3, PorterDuff.Mode.MULTIPLY));
attendance.setSeen(true);
AsyncTask.execute(() -> {
App.db.metadataDao().setSeen(App.Companion.getProfileId(), attendance, true);
//Intent i = new Intent("android.intent.action.MAIN").putExtra(MainActivity.ACTION_UPDATE_BADGES, "yes, sure");
//context.sendBroadcast(i);
});
}
else {
holder.attendanceLessonTopic.setBackground(null);
}
}
@Override
public int getItemCount() {
return attendanceList.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView attendanceType;
TextView attendanceLessonTopic;
TextView attendanceSubject;
TextView attendanceTeacher;
TextView attendanceDate;
TextView attendanceTime;
ViewHolder(View itemView) {
super(itemView);
attendanceType = itemView.findViewById(R.id.attendanceType);
attendanceLessonTopic = itemView.findViewById(R.id.attendanceLessonTopic);
attendanceSubject = itemView.findViewById(R.id.attendanceSubject);
attendanceTeacher = itemView.findViewById(R.id.attendanceTeacher);
attendanceDate = itemView.findViewById(R.id.attendanceDate);
attendanceTime = itemView.findViewById(R.id.attendanceTime);
}
}
}

View File

@ -0,0 +1,188 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-29.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance
import android.animation.ObjectAnimator
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.startCoroutineTimer
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceDayRange
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceEmpty
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceMonth
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceSubject
import pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder.*
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import kotlin.coroutines.CoroutineContext
class AttendanceAdapter(
val activity: AppCompatActivity,
val type: Int,
var onAttendanceClick: ((item: AttendanceFull) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope {
companion object {
private const val TAG = "AttendanceAdapter"
private const val ITEM_TYPE_ATTENDANCE = 0
private const val ITEM_TYPE_DAY_RANGE = 1
private const val ITEM_TYPE_MONTH = 2
private const val ITEM_TYPE_SUBJECT = 3
private const val ITEM_TYPE_EMPTY = 4
const val STATE_CLOSED = 0
const val STATE_OPENED = 1
}
private val app = activity.applicationContext as App
// optional: place the manager here
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
var items = mutableListOf<Any>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ITEM_TYPE_ATTENDANCE -> AttendanceViewHolder(inflater, parent)
ITEM_TYPE_DAY_RANGE -> DayRangeViewHolder(inflater, parent)
ITEM_TYPE_MONTH -> MonthViewHolder(inflater, parent)
ITEM_TYPE_SUBJECT -> SubjectViewHolder(inflater, parent)
ITEM_TYPE_EMPTY -> EmptyViewHolder(inflater, parent)
else -> throw IllegalArgumentException("Incorrect viewType")
}
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is AttendanceFull -> ITEM_TYPE_ATTENDANCE
is AttendanceDayRange -> ITEM_TYPE_DAY_RANGE
is AttendanceMonth -> ITEM_TYPE_MONTH
is AttendanceSubject -> ITEM_TYPE_SUBJECT
is AttendanceEmpty -> ITEM_TYPE_EMPTY
else -> throw IllegalArgumentException("Incorrect viewType")
}
}
private val onClickListener = View.OnClickListener { view ->
val model = view.getTag(R.string.tag_key_model)
if (model is AttendanceFull) {
onAttendanceClick?.invoke(model)
return@OnClickListener
}
if (model !is ExpandableItemModel<*>)
return@OnClickListener
expandModel(model, view)
}
fun expandModel(model: ExpandableItemModel<*>?, view: View?, notifyAdapter: Boolean = true) {
model ?: return
val position = items.indexOf(model)
if (position == -1)
return
view?.findViewById<View>(R.id.dropdownIcon)?.let { dropdownIcon ->
ObjectAnimator.ofFloat(
dropdownIcon,
View.ROTATION,
if (model.state == STATE_CLOSED) 0f else 180f,
if (model.state == STATE_CLOSED) 180f else 0f
).setDuration(200).start();
}
if (model is AttendanceDayRange || model is AttendanceMonth) {
// hide the preview, show summary
val preview = view?.findViewById<View>(R.id.previewContainer)
val summary = view?.findViewById<View>(R.id.summaryContainer)
val percentage = view?.findViewById<View>(R.id.percentage)
preview?.isInvisible = model.state == STATE_CLOSED
summary?.isInvisible = model.state != STATE_CLOSED
percentage?.isVisible = model.state != STATE_CLOSED
}
if (model.state == STATE_CLOSED) {
val subItems = when {
model.items.isEmpty() -> listOf(AttendanceEmpty())
else -> model.items
}
model.state = STATE_OPENED
items.addAll(position + 1, subItems.filterNotNull())
if (notifyAdapter) notifyItemRangeInserted(position + 1, subItems.size)
}
else {
val start = position + 1
var end: Int = items.size
for (i in start until items.size) {
val model1 = items[i]
val level = (model1 as? ExpandableItemModel<*>)?.level ?: 3
if (level <= model.level) {
end = i
break
} else {
if (model1 is ExpandableItemModel<*> && model1.state == STATE_OPENED) {
model1.state = STATE_CLOSED
}
}
}
if (end != -1) {
items.subList(start, end).clear()
if (notifyAdapter) notifyItemRangeRemoved(start, end - start)
}
model.state = STATE_CLOSED
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
if (holder !is BindableViewHolder<*, *>)
return
val viewType = when (holder) {
is AttendanceViewHolder -> ITEM_TYPE_ATTENDANCE
is DayRangeViewHolder -> ITEM_TYPE_DAY_RANGE
is MonthViewHolder -> ITEM_TYPE_MONTH
is SubjectViewHolder -> ITEM_TYPE_SUBJECT
is EmptyViewHolder -> ITEM_TYPE_EMPTY
else -> throw IllegalArgumentException("Incorrect viewType")
}
holder.itemView.setTag(R.string.tag_key_view_type, viewType)
holder.itemView.setTag(R.string.tag_key_position, position)
holder.itemView.setTag(R.string.tag_key_model, item)
when {
holder is AttendanceViewHolder && item is AttendanceFull -> holder.onBind(activity, app, item, position, this)
holder is DayRangeViewHolder && item is AttendanceDayRange -> holder.onBind(activity, app, item, position, this)
holder is MonthViewHolder && item is AttendanceMonth -> holder.onBind(activity, app, item, position, this)
holder is SubjectViewHolder && item is AttendanceSubject -> holder.onBind(activity, app, item, position, this)
holder is EmptyViewHolder && item is AttendanceEmpty -> holder.onBind(activity, app, item, position, this)
}
holder.itemView.setOnClickListener(onClickListener)
}
fun notifyItemChanged(model: Any) {
startCoroutineTimer(1000L, 0L) {
val index = items.indexOf(model)
if (index != -1)
notifyItemChanged(index)
}
}
override fun getItemCount() = items.size
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-5-1.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.text.TextPaint
import android.util.AttributeSet
import android.view.View
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.utils.Colors
/* https://github.com/JakubekWeg/Mobishit/blob/master/app/src/main/java/jakubweg/mobishit/view/AttendanceBarView.kt */
class AttendanceBar : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
private var attendancesList = listOf<AttendanceItem>()
private val mainPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).also {
it.textAlign = Paint.Align.CENTER
}
private var mPath = Path()
private var mCornerRadius: Float = 0.toFloat()
init {
mCornerRadius = 4.dp.toFloat()
if (isInEditMode)
setAttendanceData(mapOf(
0xff43a047.toInt() to 23,
0xff009688.toInt() to 187,
0xff3f51b5.toInt() to 46,
0xff3f51b5.toInt() to 5,
0xffffc107.toInt() to 5,
0xff9e9e9e.toInt() to 26,
0xff76ff03.toInt() to 34,
0xffff3d00.toInt() to 8
))
}
// color, count
private class AttendanceItem(var color: Int, var count: Int)
fun setAttendanceData(list: Map<Int, Int>) {
attendancesList = list.map { AttendanceItem(it.key, it.value) }
setWillNotDraw(false)
invalidate()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val r = RectF(0f, 0f, w.toFloat(), h.toFloat())
mPath = Path().apply {
addRoundRect(r, mCornerRadius, mCornerRadius, Path.Direction.CW)
close()
}
}
@SuppressLint("DrawAllocation", "CanvasSize")
override fun onDraw(canvas: Canvas?) {
canvas ?: return
val sum = attendancesList.sumBy { it.count }
if (sum == 0) {
return
}
canvas.clipPath(mPath)
val top = paddingTop.toFloat()
val bottom = (height - paddingBottom).toFloat()
var left = paddingLeft.toFloat()
val unitWidth = (width - paddingRight - paddingLeft).toFloat() / sum.toFloat()
textPaint.color = Color.BLACK
textPaint.textSize = 14.dp.toFloat()
for (e in attendancesList) {
if (e.count == 0)
continue
val width = unitWidth * e.count
mainPaint.color = e.color
canvas.drawRect(left, top, left + width, bottom, mainPaint)
val textBounds = Rect()
textPaint.getTextBounds(e.count.toString(), 0, e.count.toString().length, textBounds)
if (width > textBounds.width() + 8.dp) {
textPaint.color = Colors.legibleTextColor(e.color)
canvas.drawText(e.count.toString(), left + width / 2, bottom - height / 2 + textBounds.height()/2, textPaint)
}
left += width
}
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance
import android.os.AsyncTask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.databinding.AttendanceFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import kotlin.coroutines.CoroutineContext
class AttendanceFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "AttendanceFragment"
const val VIEW_DAYS = 0
const val VIEW_MONTHS = 1
const val VIEW_SUMMARY = 2
const val VIEW_LIST = 3
var pageSelection = 1
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: AttendanceFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = AttendanceFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!isAdded) return
activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_attendance_config)
.withIcon(CommunityMaterial.Icon2.cmd_settings_outline)
.withOnClickListener(View.OnClickListener {
activity.bottomSheet.close()
AttendanceConfigDialog(activity, true, null, null)
}),
BottomSheetSeparatorItem(true),
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(View.OnClickListener {
activity.bottomSheet.close()
AsyncTask.execute { App.db.metadataDao().setAllSeen(App.profileId, Metadata.TYPE_ATTENDANCE, true) }
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
})
)
activity.gainAttention()
if (pageSelection == 1)
pageSelection = app.config.forProfile().attendance.attendancePageSelection
val pagerAdapter = FragmentLazyPagerAdapter(
fragmentManager ?: return,
b.refreshLayout,
listOf(
AttendanceSummaryFragment() to getString(R.string.attendance_tab_summary),
AttendanceListFragment().apply {
arguments = Bundle("viewType" to VIEW_DAYS)
} to getString(R.string.attendance_tab_days),
AttendanceListFragment().apply {
arguments = Bundle("viewType" to VIEW_MONTHS)
} to getString(R.string.attendance_tab_months),
AttendanceListFragment().apply {
arguments = Bundle("viewType" to VIEW_LIST)
} to getString(R.string.attendance_tab_list)
)
)
b.viewPager.apply {
offscreenPageLimit = 1
adapter = pagerAdapter
currentItem = pageSelection
addOnPageSelectedListener {
pageSelection = it
app.config.forProfile().attendance.attendancePageSelection = it
}
b.tabLayout.setupWithViewPager(this)
}
}
}

View File

@ -51,7 +51,7 @@ import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_RELEASED
import static pl.szczodrzynski.edziennik.data.db.entity.LoginStore.LOGIN_TYPE_VULCAN; import static pl.szczodrzynski.edziennik.data.db.entity.LoginStore.LOGIN_TYPE_VULCAN;
import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ATTENDANCE; import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ATTENDANCE;
public class AttendanceFragment extends Fragment { public class AttendanceFragment_ extends Fragment {
private App app = null; private App app = null;
private MainActivity activity = null; private MainActivity activity = null;
@ -278,11 +278,12 @@ public class AttendanceFragment extends Fragment {
b.attendanceView.setVisibility(View.VISIBLE); b.attendanceView.setVisibility(View.VISIBLE);
b.attendanceNoData.setVisibility(View.GONE); b.attendanceNoData.setVisibility(View.GONE);
if ((adapter = (AttendanceAdapter) b.attendanceView.getAdapter()) != null) { if ((adapter = (AttendanceAdapter) b.attendanceView.getAdapter()) != null) {
adapter.attendanceList = filteredList; //adapter.setItems(filteredList);
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
else { else {
adapter = new AttendanceAdapter(getContext(), filteredList); //adapter = new AttendanceAdapter(activity, true, null);
//adapter.setItems(filteredList);
b.attendanceView.setAdapter(adapter); b.attendanceView.setAdapter(adapter);
} }
} }

View File

@ -0,0 +1,203 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.databinding.AttendanceListFragmentBinding
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.startCoroutineTimer
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceDayRange
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceMonth
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext
class AttendanceListFragment : LazyFragment(), CoroutineScope {
companion object {
private const val TAG = "AttendanceListFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: AttendanceListFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
private val manager by lazy { app.attendanceManager }
private var viewType = AttendanceFragment.VIEW_DAYS
private var expandSubjectId = 0L
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = AttendanceListFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root
}
override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
if (!isAdded) return@startCoroutineTimer
viewType = arguments?.getInt("viewType") ?: AttendanceFragment.VIEW_DAYS
expandSubjectId = arguments?.getLong("gradesSubjectId") ?: 0L
val adapter = AttendanceAdapter(activity, viewType)
var firstRun = true
app.db.attendanceDao().getAll(App.profileId).observe(this@AttendanceListFragment, Observer { items -> this@AttendanceListFragment.launch {
if (!isAdded) return@launch
// load & configure the adapter
adapter.items = withContext(Dispatchers.Default) { processAttendance(items) }
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
b.list.adapter = adapter
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addOnScrollListener(onScrollListener)
}
}
adapter.notifyDataSetChanged()
if (firstRun) {
expandSubject(adapter)
firstRun = false
}
// show/hide relevant views
b.progressBar.isVisible = false
if (items.isNullOrEmpty()) {
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.list.isVisible = true
b.noData.isVisible = false
}
}})
adapter.onAttendanceClick = {
//GradeDetailsDialog(activity, it)
}
}; return true}
private fun expandSubject(adapter: AttendanceAdapter) {
var expandSubjectModel: GradesSubject? = null
if (expandSubjectId != 0L) {
expandSubjectModel = adapter.items.firstOrNull { it is GradesSubject && it.subjectId == expandSubjectId } as? GradesSubject
adapter.expandModel(
model = expandSubjectModel,
view = null,
notifyAdapter = false
)
}
startCoroutineTimer(500L) {
if (expandSubjectModel != null) {
b.list.smoothScrollToPosition(
adapter.items.indexOf(expandSubjectModel) + expandSubjectModel.semesters.size + (expandSubjectModel.semesters.firstOrNull()?.grades?.size ?: 0)
)
}
}
}
@Suppress("SuspendFunctionOnCoroutineScope")
private fun processAttendance(attendance: List<AttendanceFull>): MutableList<Any> {
if (attendance.isEmpty())
return mutableListOf()
val groupConsecutiveDays = app.config.forProfile().attendance.groupConsecutiveDays
val showPresenceInMonth = app.config.forProfile().attendance.showPresenceInMonth
if (viewType == AttendanceFragment.VIEW_DAYS) {
val items = attendance
.filter { it.baseType != Attendance.TYPE_PRESENT }
.groupBy { it.date }
.map { AttendanceDayRange(
rangeStart = it.key,
rangeEnd = null,
items = it.value.toMutableList()
) }
.toMutableList()
if (groupConsecutiveDays) {
items.sortByDescending { it.rangeStart }
val iterator = items.listIterator()
var element = iterator.next()
while (iterator.hasNext()) {
var nextElement = iterator.next()
while (Date.diffDays(element.rangeStart, nextElement.rangeStart) <= 1) {
if (element.rangeEnd == null)
element.rangeEnd = element.rangeStart
element.items.addAll(nextElement.items)
element.rangeStart = nextElement.rangeStart
iterator.remove()
nextElement = iterator.next()
}
element = nextElement
}
}
return items.toMutableList()
}
else if (viewType == AttendanceFragment.VIEW_MONTHS) {
val items = attendance
.groupBy { it.date.year to it.date.month }
.map { AttendanceMonth(
year = it.key.first,
month = it.key.second,
items = it.value.toMutableList()
) }
items.forEach { month ->
month.typeCountMap = month.items
.groupBy { it.baseType }
.map { it.key to it.value.size }
.sortedBy { it.first }
.toMap()
val totalCount = month.typeCountMap.entries.sumBy { it.value }
val presenceCount = month.typeCountMap.entries.sumBy {
when (it.key) {
Attendance.TYPE_PRESENT,
Attendance.TYPE_PRESENT_CUSTOM,
Attendance.TYPE_BELATED,
Attendance.TYPE_BELATED_EXCUSED,
Attendance.TYPE_RELEASED -> it.value
else -> 0
}
}
month.percentage = if (totalCount == 0)
0f
else
presenceCount.toFloat() / totalCount.toFloat() * 100f
if (!showPresenceInMonth)
month.items.removeAll { it.baseType == Attendance.TYPE_PRESENT }
}
return items.toMutableList()
}
return attendance.filter { it.baseType != Attendance.TYPE_PRESENT }.toMutableList()
}
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-5-4.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.databinding.AttendanceListFragmentBinding
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.models.AttendanceSubject
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject
import kotlin.coroutines.CoroutineContext
class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
companion object {
private const val TAG = "AttendanceSummaryFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: AttendanceListFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
private val manager by lazy { app.attendanceManager }
private var expandSubjectId = 0L
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = AttendanceListFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root
}
override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
if (!isAdded) return@startCoroutineTimer
expandSubjectId = arguments?.getLong("gradesSubjectId") ?: 0L
val adapter = AttendanceAdapter(activity, VIEW_SUMMARY)
var firstRun = true
app.db.attendanceDao().getAll(App.profileId).observe(this@AttendanceSummaryFragment, Observer { items -> this@AttendanceSummaryFragment.launch {
if (!isAdded) return@launch
// load & configure the adapter
adapter.items = withContext(Dispatchers.Default) { processAttendance(items) }
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
b.list.adapter = adapter
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addOnScrollListener(onScrollListener)
}
}
adapter.notifyDataSetChanged()
if (firstRun) {
expandSubject(adapter)
firstRun = false
}
// show/hide relevant views
b.progressBar.isVisible = false
if (items.isNullOrEmpty()) {
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.list.isVisible = true
b.noData.isVisible = false
}
}})
adapter.onAttendanceClick = {
//GradeDetailsDialog(activity, it)
}
}; return true}
private fun expandSubject(adapter: AttendanceAdapter) {
var expandSubjectModel: GradesSubject? = null
if (expandSubjectId != 0L) {
expandSubjectModel = adapter.items.firstOrNull { it is GradesSubject && it.subjectId == expandSubjectId } as? GradesSubject
adapter.expandModel(
model = expandSubjectModel,
view = null,
notifyAdapter = false
)
}
startCoroutineTimer(500L) {
if (expandSubjectModel != null) {
b.list.smoothScrollToPosition(
adapter.items.indexOf(expandSubjectModel) + expandSubjectModel.semesters.size + (expandSubjectModel.semesters.firstOrNull()?.grades?.size ?: 0)
)
}
}
}
@Suppress("SuspendFunctionOnCoroutineScope")
private fun processAttendance(attendance: List<AttendanceFull>): MutableList<Any> {
if (attendance.isEmpty())
return mutableListOf()
val items = attendance
.groupBy { it.subjectId }
.map { AttendanceSubject(
subjectId = it.key,
subjectName = it.value.firstOrNull()?.subjectLongName ?: "",
items = it.value.toMutableList()
) }
.sortedBy { it.subjectName.toLowerCase() }
items.forEach { subject ->
subject.typeCountMap = subject.items
.groupBy { it.baseType }
.map { it.key to it.value.size }
.sortedBy { it.first }
.toMap()
val totalCount = subject.typeCountMap.entries.sumBy { it.value }
val presenceCount = subject.typeCountMap.entries.sumBy {
when (it.key) {
Attendance.TYPE_PRESENT,
Attendance.TYPE_PRESENT_CUSTOM,
Attendance.TYPE_BELATED,
Attendance.TYPE_BELATED_EXCUSED,
Attendance.TYPE_RELEASED -> it.value
else -> 0
}
}
subject.percentage = if (totalCount == 0)
0f
else
presenceCount.toFloat() / totalCount.toFloat() * 100f
if (!false /* showPresenceInSubject */)
subject.items.removeAll { it.baseType == Attendance.TYPE_PRESENT }
}
return items.toMutableList()
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-29.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance
import android.annotation.SuppressLint
import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.graphics.ColorUtils
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.setTintColor
import pl.szczodrzynski.edziennik.utils.managers.AttendanceManager
class AttendanceView : AppCompatTextView {
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr)
constructor(context: Context, attendance: Attendance, manager: AttendanceManager) : this(context, null) {
setAttendance(attendance, manager, false)
}
@SuppressLint("RestrictedApi")
fun setAttendance(attendance: Attendance?, manager: AttendanceManager, bigView: Boolean = false) {
if (attendance == null) {
visibility = View.GONE
return
}
visibility = View.VISIBLE
val attendanceName = if (manager.useSymbols)
attendance.typeSymbol
else
attendance.typeShort
val attendanceColor = manager.getAttendanceColor(attendance)
text = when {
attendanceName.isBlank() -> " "
else -> attendanceName
}
setTextColor(if (ColorUtils.calculateLuminance(attendanceColor) > 0.3)
0xaa000000.toInt()
else
0xccffffff.toInt())
setBackgroundResource(if (bigView) R.drawable.bg_rounded_8dp else R.drawable.bg_rounded_4dp)
background.setTintColor(attendanceColor)
gravity = Gravity.CENTER
if (bigView) {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 22f)
setAutoSizeTextTypeUniformWithConfiguration(
14,
32,
1,
TypedValue.COMPLEX_UNIT_SP
)
setPadding(2.dp, 2.dp, 2.dp, 2.dp)
}
else {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
setPadding(5.dp, 0, 5.dp, 0)
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, 0, 5.dp, 0)
}
maxLines = 1
ellipsize = TextUtils.TruncateAt.END
measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.models
class AttendanceCount {
var normalSum = 0f
var normalCount = 0
var normalWeightedSum = 0f
var normalWeightedCount = 0f
var pointSum = 0f
var pointAvgSum = 0f
var pointAvgMax = 0f
var normalAvg: Float? = null
var pointAvgPercent: Float? = null
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.models
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
import pl.szczodrzynski.edziennik.utils.models.Date
data class AttendanceDayRange(
var rangeStart: Date,
var rangeEnd: Date?,
override val items: MutableList<AttendanceFull> = mutableListOf()
) : ExpandableItemModel<AttendanceFull>(items) {
override var level = 1
var lastAddedDate = 0L
var hasUnseen: Boolean = false
get() = field || items.any { it.baseType != Attendance.TYPE_PRESENT && !it.seen }
}

View File

@ -0,0 +1,7 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-5-4.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.models
class AttendanceEmpty

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.models
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
data class AttendanceMonth(
val year: Int,
val month: Int,
override val items: MutableList<AttendanceFull> = mutableListOf()
) : ExpandableItemModel<AttendanceFull>(items) {
override var level = 1
var lastAddedDate = 0L
var hasUnseen: Boolean = false
get() = field || items.any { it.baseType != Attendance.TYPE_PRESENT && !it.seen }
var typeCountMap: Map<Int, Int> = mapOf()
var percentage: Float = 0f
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-5-4.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.models
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
data class AttendanceSubject(
val subjectId: Long,
val subjectName: String,
override val items: MutableList<AttendanceFull> = mutableListOf()
) : ExpandableItemModel<AttendanceFull>(items) {
override var level = 1
var lastAddedDate = 0L
var hasUnseen: Boolean = false
get() = field || items.any { it.baseType != Attendance.TYPE_PRESENT && !it.seen }
var typeCountMap: Map<Int, Int> = mapOf()
var percentage: Float = 0f
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.concat
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.databinding.AttendanceItemAttendanceBinding
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceDayRange
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceMonth
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import pl.szczodrzynski.edziennik.utils.Themes
class AttendanceViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
val b: AttendanceItemAttendanceBinding = AttendanceItemAttendanceBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<AttendanceFull, AttendanceAdapter> {
companion object {
private const val TAG = "AttendanceViewHolder"
}
override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceFull, position: Int, adapter: AttendanceAdapter) {
val manager = app.attendanceManager
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
val bullet = ""
b.attendanceView.setAttendance(item, manager, bigView = true)
b.type.text = item.typeName
b.subjectName.text = item.subjectLongName ?: item.lessonTopic
b.dateTime.text = listOf(
item.date.formattedStringShort,
item.startTime?.stringHM,
item.lessonNumber?.let { app.getString(R.string.attendance_lesson_number_format, it) }
).concat(bullet)
if (item.showAsUnseen == null)
item.showAsUnseen = !item.seen
b.unread.isVisible = item.showAsUnseen == true
if (!item.seen) {
manager.markAsSeen(item)
val container = adapter.items.firstOrNull {
it is ExpandableItemModel<*> && it.items.contains(item)
} as? ExpandableItemModel<*> ?: return
var hasUnseen = true
if (container is AttendanceDayRange) {
hasUnseen = container.items.any { !it.seen }
container.hasUnseen = hasUnseen
}
if (container is AttendanceMonth) {
hasUnseen = container.items.any { !it.seen }
container.hasUnseen = hasUnseen
}
// check if the unseen status has changed
if (!hasUnseen) {
adapter.notifyItemChanged(container)
}
}
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.concat
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.databinding.AttendanceItemContainerBinding
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.AttendanceView
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceDayRange
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import pl.szczodrzynski.edziennik.utils.Themes
class DayRangeViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
val b: AttendanceItemContainerBinding = AttendanceItemContainerBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<AttendanceDayRange, AttendanceAdapter> {
companion object {
private const val TAG = "DayRangeViewHolder"
}
override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceDayRange, position: Int, adapter: AttendanceAdapter) {
val manager = app.attendanceManager
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.title.text = listOf(
item.rangeStart.formattedString,
item.rangeEnd?.formattedString
).concat(" - ")
b.dropdownIcon.rotation = when (item.state) {
STATE_CLOSED -> 0f
else -> 180f
}
b.unread.isVisible = item.hasUnseen
b.previewContainer.visibility = if (item.state == STATE_CLOSED) View.VISIBLE else View.INVISIBLE
b.summaryContainer.visibility = if (item.state == STATE_CLOSED) View.INVISIBLE else View.VISIBLE
b.previewContainer.removeAllViews()
for (attendance in item.items) {
if (attendance.baseType == Attendance.TYPE_PRESENT_CUSTOM || attendance.baseType == Attendance.TYPE_UNKNOWN)
continue
b.previewContainer.addView(AttendanceView(
contextWrapper,
attendance,
manager
))
}
if (item.items.isEmpty() || item.items.none { it.baseType != Attendance.TYPE_PRESENT_CUSTOM && it.baseType != Attendance.TYPE_UNKNOWN }) {
b.previewContainer.addView(TextView(contextWrapper).also {
it.setText(R.string.attendance_empty_text)
})
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-5-4.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.AttendanceItemEmptyBinding
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceEmpty
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
class EmptyViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
val b: AttendanceItemEmptyBinding = AttendanceItemEmptyBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<AttendanceEmpty, AttendanceAdapter> {
companion object {
private const val TAG = "EmptyViewHolder"
}
override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceEmpty, position: Int, adapter: AttendanceAdapter) {
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.databinding.AttendanceItemContainerBarBinding
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.AttendanceView
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceMonth
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
class MonthViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
val b: AttendanceItemContainerBarBinding = AttendanceItemContainerBarBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<AttendanceMonth, AttendanceAdapter> {
companion object {
private const val TAG = "MonthViewHolder"
}
override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceMonth, position: Int, adapter: AttendanceAdapter) {
val manager = app.attendanceManager
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.title.text = listOf(
app.resources.getStringArray(R.array.material_calendar_months_array).getOrNull(item.month - 1)?.fixName(),
item.year.toString()
).concat(" ")
b.dropdownIcon.rotation = when (item.state) {
STATE_CLOSED -> 0f
else -> 180f
}
b.unread.isVisible = item.hasUnseen
b.attendanceBar.setAttendanceData(item.typeCountMap.mapKeys { manager.getAttendanceColor(it.key) })
b.previewContainer.isInvisible = item.state != STATE_CLOSED
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) {
b.percentage.isVisible = false
b.percentage.text = null
b.summaryContainer.isVisible = false
b.summaryContainer.text = null
}
else {
b.percentage.setText(R.string.attendance_percentage_format, item.percentage)
b.summaryContainer.setText(R.string.attendance_period_summary_format, item.percentage)
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-5-4.
*/
package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.databinding.AttendanceItemContainerBarBinding
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.setText
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.AttendanceView
import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceSubject
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
class SubjectViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
val b: AttendanceItemContainerBarBinding = AttendanceItemContainerBarBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<AttendanceSubject, AttendanceAdapter> {
companion object {
private const val TAG = "SubjectViewHolder"
}
override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceSubject, position: Int, adapter: AttendanceAdapter) {
val manager = app.attendanceManager
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.title.text = item.subjectName
b.dropdownIcon.rotation = when (item.state) {
STATE_CLOSED -> 0f
else -> 180f
}
b.unread.isVisible = item.hasUnseen
b.attendanceBar.setAttendanceData(item.typeCountMap.mapKeys { manager.getAttendanceColor(it.key) })
b.previewContainer.isInvisible = item.state != STATE_CLOSED
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) {
b.percentage.isVisible = false
b.percentage.text = null
b.summaryContainer.isVisible = false
b.summaryContainer.text = null
}
else {
b.percentage.setText(R.string.attendance_percentage_format, item.percentage)
b.summaryContainer.setText(R.string.attendance_period_summary_format, item.percentage)
}
}
}

View File

@ -6,7 +6,7 @@ package pl.szczodrzynski.edziennik.ui.modules.grades.models
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter.Companion.STATE_CLOSED import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter.Companion.STATE_CLOSED
abstract class ExpandableItemModel<T>(val items: MutableList<T>) { abstract class ExpandableItemModel<T>(open val items: MutableList<T>) {
open var level: Int = 3 open var level: Int = 3
var state: Int = STATE_CLOSED var state: Int = STATE_CLOSED
} }

View File

@ -52,6 +52,7 @@ import pl.szczodrzynski.edziennik.network.NetworkUtils;
import pl.szczodrzynski.edziennik.sync.SyncWorker; import pl.szczodrzynski.edziennik.sync.SyncWorker;
import pl.szczodrzynski.edziennik.sync.UpdateWorker; import pl.szczodrzynski.edziennik.sync.UpdateWorker;
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog; import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog; import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog; import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.sync.NotificationFilterDialog; import pl.szczodrzynski.edziennik.ui.dialogs.sync.NotificationFilterDialog;
@ -885,6 +886,15 @@ public class SettingsNewFragment extends MaterialAboutFragment {
.color(IconicsColor.colorInt(iconColor)) .color(IconicsColor.colorInt(iconColor))
).setOnClickAction(() -> new GradesConfigDialog(activity, false, null, null))); ).setOnClickAction(() -> new GradesConfigDialog(activity, false, null, null)));
items.add(new MaterialAboutActionItem(
getString(R.string.menu_attendance_config),
null,
new IconicsDrawable(activity)
.icon(CommunityMaterial.Icon.cmd_calendar_remove_outline)
.size(IconicsSize.dp(iconSizeDp))
.color(IconicsColor.colorInt(iconColor))
).setOnClickAction(() -> new AttendanceConfigDialog(activity, false, null, null)));
registerCardAllowRegistrationItem = new MaterialAboutSwitchItem( registerCardAllowRegistrationItem = new MaterialAboutSwitchItem(
getString(R.string.settings_register_allow_registration_text), getString(R.string.settings_register_allow_registration_text),
getString(R.string.settings_register_allow_registration_subtext), getString(R.string.settings_register_allow_registration_subtext),

View File

@ -19,10 +19,13 @@ class AttendanceManager(val app: App) : CoroutineScope {
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default get() = job + Dispatchers.Default
val useSymbols
get() = app.config.forProfile().attendance.useSymbols
fun getTypeShort(baseType: Int): String { fun getTypeShort(baseType: Int): String {
return when (baseType) { return when (baseType) {
Attendance.TYPE_PRESENT -> "ob" Attendance.TYPE_PRESENT -> "ob"
Attendance.TYPE_PRESENT_CUSTOM -> "ob?" Attendance.TYPE_PRESENT_CUSTOM -> " "
Attendance.TYPE_ABSENT -> "nb" Attendance.TYPE_ABSENT -> "nb"
Attendance.TYPE_ABSENT_EXCUSED -> "u" Attendance.TYPE_ABSENT_EXCUSED -> "u"
Attendance.TYPE_RELEASED -> "zw" Attendance.TYPE_RELEASED -> "zw"
@ -33,6 +36,26 @@ class AttendanceManager(val app: App) : CoroutineScope {
} }
} }
fun getAttendanceColor(baseType: Int): Int {
return when (baseType) {
Attendance.TYPE_PRESENT -> 0xff009688.toInt()
Attendance.TYPE_PRESENT_CUSTOM -> 0xff64b5f6.toInt()
Attendance.TYPE_ABSENT -> 0xffff3d00.toInt()
Attendance.TYPE_ABSENT_EXCUSED -> 0xff76ff03.toInt()
Attendance.TYPE_RELEASED -> 0xff9e9e9e.toInt()
Attendance.TYPE_BELATED -> 0xffffc107.toInt()
Attendance.TYPE_BELATED_EXCUSED -> 0xffffc107.toInt()
Attendance.TYPE_DAY_FREE -> 0xff43a047.toInt()
else -> 0xff64b5f6.toInt()
}
}
fun getAttendanceColor(attendance: Attendance): Int {
return (if (useSymbols) attendance.typeColor else null) ?: when (attendance.baseType) {
Attendance.TYPE_PRESENT_CUSTOM -> attendance.typeColor ?: 0xff64b5f6.toInt()
else -> getAttendanceColor(attendance.baseType)
}
}
/* _ _ _____ _____ _ __ _ /* _ _ _____ _____ _ __ _
| | | |_ _| / ____| (_)/ _(_) | | | |_ _| / ____| (_)/ _(_)
| | | | | | | (___ _ __ ___ ___ _| |_ _ ___ | | | | | | | (___ _ __ ___ ___ _| |_ _ ___

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-5-4.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<TextView
style="@style/TextAppearance.AppCompat.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:text="@string/attendance_config_title" />
<CheckBox
android:id="@+id/useSymbols"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="32dp"
android:text="@string/attendance_config_use_symbols" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="4dp"
android:text="@string/attendance_config_use_symbols_hint"
android:textAppearance="@style/NavView.TextView.Helper"
android:textSize="12sp"
android:textStyle="italic" />
<CheckBox
android:id="@+id/groupConsecutiveDays"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="32dp"
android:text="@string/attendance_config_group_consecutive_days" />
<CheckBox
android:id="@+id/showPresenceInMonth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="32dp"
android:text="@string/attendance_config_show_presence_in_month"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-4-30.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorSurface_6dp"
app:tabIndicatorColor="?colorPrimary"
app:tabMode="auto"
app:tabSelectedTextColor="?colorPrimary"
app:tabTextColor="?android:textColorPrimary" />
<pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout>

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-4-29.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="simpleMode"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:baselineAligned="false"
android:orientation="horizontal"
android:paddingVertical="8dp">
<pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceView
android:id="@+id/attendanceView"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="@drawable/bg_rounded_8dp"
tools:backgroundTint="#f44336"
tools:gravity="center"
tools:text="nb"
tools:textSize="22sp" />
<TextView
android:id="@+id/type"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:ellipsize="middle"
android:maxLines="2"
android:singleLine="true"
android:textAppearance="@style/NavView.TextView.Helper"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@+id/unread"
app:layout_constraintStart_toEndOf="@+id/attendanceView"
app:layout_constraintTop_toTopOf="parent"
tools:text="Nieobecność nieusprawiedliwiona" />
<TextView
android:id="@+id/subjectName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:ellipsize="middle"
android:maxLines="2"
android:singleLine="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/attendanceView"
app:layout_constraintTop_toBottomOf="@+id/type"
tools:text="Język angielski" />
<TextView
android:id="@+id/dateTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:ellipsize="middle"
android:maxLines="2"
android:singleLine="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/attendanceView"
app:layout_constraintTop_toBottomOf="@+id/subjectName"
tools:text="2 marca 2019 • 10:45 • lekcja 4" />
<View
android:id="@+id/unread"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginHorizontal="8dp"
android:background="@drawable/unread_red_circle"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/type"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-4-30.
-->
<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="2 marca - 3 marca" />
<View
android:id="@+id/unread"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:visibility="gone"
android:background="@drawable/unread_red_circle"
tools:visibility="visible"/>
<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>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp">
<LinearLayout
android:id="@+id/previewContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:orientation="horizontal"
tools:visibility="visible" />
<TextView
android:id="@+id/summaryContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textSize="14sp"
android:visibility="gone"
tools:text="6 nieob. • 2 nieob. nieuspr. • 5 spóźnień"
tools:text1="Cały rok: 3 oceny • suma: 320 pkt"
tools:text2="Cały rok: 15 ocen • średnia: 2,62"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>
</layout>

View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-5-1.
-->
<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="Kwiecień 2020" />
<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="99,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="20dp"
android:layout_marginHorizontal="8dp"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/previewContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:orientation="horizontal"
android:visibility="gone"
app:flexWrap="wrap"
tools:visibility="visible">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="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>
<TextView
android:id="@+id/summaryContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:paddingVertical="8dp"
tools:text="Obecność w tym okresie: 100%" />
</FrameLayout>
</LinearLayout>
</layout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-5-4.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:gravity="center"
android:textSize="18sp"
android:textColor="?android:textColorSecondary"
android:textStyle="italic"
android:text="@string/attendance_empty_text"/>
</LinearLayout>
</layout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-4-30.
-->
<layout 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">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/noData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/attendances_no_data"
android:textSize="24sp"
android:visibility="gone"
app:drawableTopCompat="@drawable/ic_no_grades"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/attendance_item_attendance"
tools:visibility="visible" />
</FrameLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-5-4.
-->
<layout 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">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/noData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/attendances_no_data"
android:textSize="24sp"
android:visibility="gone"
app:drawableTopCompat="@drawable/ic_no_grades"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/attendance_item_attendance"
tools:visibility="visible" />
</FrameLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout>

View File

@ -16,6 +16,6 @@
android:textSize="18sp" android:textSize="18sp"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textStyle="italic" android:textStyle="italic"
android:text="Nie ma ocen w tym semestrze."/> android:text="@string/grades_empty_text"/>
</LinearLayout> </LinearLayout>
</layout> </layout>

View File

@ -69,7 +69,7 @@
<string name="attendances_absent">Nieobecności:</string> <string name="attendances_absent">Nieobecności:</string>
<string name="attendances_absent_unexcused">W tym nieusprawiedliwione:</string> <string name="attendances_absent_unexcused">W tym nieusprawiedliwione:</string>
<string name="attendances_belated">Spóźnienia:</string> <string name="attendances_belated">Spóźnienia:</string>
<string name="attendances_no_data">Brak nieobecności.</string> <string name="attendances_no_data">Nie masz żadnych nieobecności.</string>
<string name="attendances_present">Obecności:</string> <string name="attendances_present">Obecności:</string>
<string name="attendances_releases">Zwolnienia:</string> <string name="attendances_releases">Zwolnienia:</string>
<string name="attendances_summary_subject_loading_format">Wszystkie przedmioty</string> <string name="attendances_summary_subject_loading_format">Wszystkie przedmioty</string>
@ -1288,4 +1288,19 @@
<string name="you_are_offline_title">Połączenie sieciowe</string> <string name="you_are_offline_title">Połączenie sieciowe</string>
<string name="settings_add_student_text">Dodaj nowego ucznia</string> <string name="settings_add_student_text">Dodaj nowego ucznia</string>
<string name="settings_add_student_subtext">Zaloguj konto ucznia/rodzica w aplikacji</string> <string name="settings_add_student_subtext">Zaloguj konto ucznia/rodzica w aplikacji</string>
<string name="attendance_lesson_number_format">lekcja %d</string>
<string name="menu_attendance_config">Ustawienia frekwencji</string>
<string name="attendance_tab_days">Dni</string>
<string name="attendance_tab_months">Miesiące</string>
<string name="attendance_tab_summary">Podsumowanie</string>
<string name="attendance_tab_list">Lista</string>
<string name="attendance_period_summary_format">Obecność w tym okresie: %.2f%%</string>
<string name="attendance_percentage_format">%.2f%%</string>
<string name="grades_empty_text">Nie ma ocen w tym semestrze.</string>
<string name="attendance_empty_text">Nie ma tutaj żadnych nieobecności.</string>
<string name="attendance_config_title">Konfiguracja frekwencji</string>
<string name="attendance_config_use_symbols">Używaj symboli i kolorów wg dziennika</string>
<string name="attendance_config_group_consecutive_days">Grupuj kolejne dni na liście</string>
<string name="attendance_config_show_presence_in_month">Wyświetlaj obecność w widoku miesięcy</string>
<string name="attendance_config_use_symbols_hint">Widoczne po rozwinięciu listy</string>
</resources> </resources>

View File

@ -17,7 +17,7 @@ buildscript {
] ]
versions = [ versions = [
gradleAndroid : "4.0.0-beta03", gradleAndroid : '4.0.0-beta05',
kotlin : ext.kotlin_version, kotlin : ext.kotlin_version,
ktx : "1.2.0", ktx : "1.2.0",