From 9167d53a1a2bd95221f2ce30e69f400718f84c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 4 May 2020 22:47:27 +0200 Subject: [PATCH] [UI] Add new attendance UI module. --- .../edziennik/config/ProfileConfig.kt | 1 + .../config/ProfileConfigAttendance.kt | 30 +++ .../edziennik/data/db/dao/AttendanceDao.kt | 2 +- .../edziennik/data/db/dao/BaseDao.kt | 4 +- .../edziennik/data/db/entity/Attendance.kt | 23 +- .../settings/AttendanceConfigDialog.kt | 75 +++++++ .../modules/attendance/AttendanceAdapter.java | 135 ------------ .../modules/attendance/AttendanceAdapter.kt | 188 ++++++++++++++++ .../ui/modules/attendance/AttendanceBar.kt | 104 +++++++++ .../modules/attendance/AttendanceFragment.kt | 112 ++++++++++ ...Fragment.java => AttendanceFragment_.java} | 7 +- .../attendance/AttendanceListFragment.kt | 203 ++++++++++++++++++ .../attendance/AttendanceSummaryFragment.kt | 162 ++++++++++++++ .../ui/modules/attendance/AttendanceView.kt | 84 ++++++++ .../attendance/models/AttendanceCount.kt | 20 ++ .../attendance/models/AttendanceDayRange.kt | 23 ++ .../attendance/models/AttendanceEmpty.kt | 7 + .../attendance/models/AttendanceMonth.kt | 25 +++ .../attendance/models/AttendanceSubject.kt | 25 +++ .../viewholder/AttendanceViewHolder.kt | 77 +++++++ .../viewholder/DayRangeViewHolder.kt | 72 +++++++ .../attendance/viewholder/EmptyViewHolder.kt | 29 +++ .../attendance/viewholder/MonthViewHolder.kt | 98 +++++++++ .../viewholder/SubjectViewHolder.kt | 98 +++++++++ .../grades/models/ExpandableItemModel.kt | 2 +- .../modules/settings/SettingsNewFragment.java | 10 + .../utils/managers/AttendanceManager.kt | 25 ++- .../res/layout/attendance_config_dialog.xml | 59 +++++ .../main/res/layout/attendance_fragment.xml | 36 ++++ .../res/layout/attendance_item_attendance.xml | 95 ++++++++ .../res/layout/attendance_item_container.xml | 94 ++++++++ .../layout/attendance_item_container_bar.xml | 128 +++++++++++ .../main/res/layout/attendance_item_empty.xml | 21 ++ .../res/layout/attendance_list_fragment.xml | 47 ++++ .../layout/attendance_summary_fragment.xml | 47 ++++ app/src/main/res/layout/grades_item_empty.xml | 2 +- app/src/main/res/values/strings.xml | 17 +- build.gradle | 2 +- 38 files changed, 2034 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigAttendance.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AttendanceConfigDialog.kt delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.java create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceBar.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.kt rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/{AttendanceFragment.java => AttendanceFragment_.java} (98%) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceView.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceCount.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceDayRange.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceEmpty.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceMonth.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceSubject.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/AttendanceViewHolder.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/DayRangeViewHolder.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/EmptyViewHolder.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/MonthViewHolder.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/SubjectViewHolder.kt create mode 100644 app/src/main/res/layout/attendance_config_dialog.xml create mode 100644 app/src/main/res/layout/attendance_fragment.xml create mode 100644 app/src/main/res/layout/attendance_item_attendance.xml create mode 100644 app/src/main/res/layout/attendance_item_container.xml create mode 100644 app/src/main/res/layout/attendance_item_container_bar.xml create mode 100644 app/src/main/res/layout/attendance_item_empty.xml create mode 100644 app/src/main/res/layout/attendance_list_fragment.xml create mode 100644 app/src/main/res/layout/attendance_summary_fragment.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt index d956c1ec..c4c65ac3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt @@ -30,6 +30,7 @@ class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List { fun getByIdNow(profileId: Int, id: Long) = 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?) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt index 94677d70..0ae961db 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt @@ -116,6 +116,8 @@ interface BaseDao { if (forceReplace) replaceAll(items) else - upsertAll(items) + upsertAll(items, removeNotKept = false) + + if (removeNotKept) removeNotKept() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Attendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Attendance.kt index af165b07..99953548 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Attendance.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Attendance.kt @@ -47,15 +47,18 @@ open class Attendance( var addedDate: Long = System.currentTimeMillis() ) : Keepable() { companion object { - const val TYPE_UNKNOWN = -1 - const val TYPE_PRESENT = 0 - const val TYPE_PRESENT_CUSTOM = 10 // count as presence AND show in the list - const val TYPE_ABSENT = 1 - const val TYPE_ABSENT_EXCUSED = 2 - const val TYPE_RELEASED = 3 - const val TYPE_BELATED = 4 - const val TYPE_BELATED_EXCUSED = 5 - const val TYPE_DAY_FREE = 6 + const val TYPE_UNKNOWN = -1 // #3f51b5 + const val TYPE_PRESENT = 0 // #009688 + const val TYPE_PRESENT_CUSTOM = 10 // count as presence AND show in the list + custom color, fallback: #3f51b5 + const val TYPE_ABSENT = 1 // #ff3d00 + const val TYPE_ABSENT_EXCUSED = 2 // #76ff03 + const val TYPE_RELEASED = 3 // #9e9e9e + const val TYPE_BELATED = 4 // #ffc107 + const val TYPE_BELATED_EXCUSED = 5 // #ffc107 + 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") @@ -64,5 +67,5 @@ open class Attendance( var lessonNumber: Int? = null @Ignore - var showAsUnseen = false + var showAsUnseen: Boolean? = null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AttendanceConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AttendanceConfigDialog.kt new file mode 100644 index 00000000..e7d66ed5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AttendanceConfigDialog.kt @@ -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 + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.java deleted file mode 100644 index 3b7fa0e9..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.java +++ /dev/null @@ -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 { - private Context context; - public List attendanceList; - - //getting the context and product list with constructor - public AttendanceAdapter(Context mCtx, List 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); - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.kt new file mode 100644 index 00000000..812e8465 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.kt @@ -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(), 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() + + 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(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(R.id.previewContainer) + val summary = view?.findViewById(R.id.summaryContainer) + val percentage = view?.findViewById(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 +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceBar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceBar.kt new file mode 100644 index 00000000..5fcdc8e0 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceBar.kt @@ -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() + 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) { + 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 + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.kt new file mode 100644 index 00000000..e3d43c7e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.kt @@ -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) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment_.java similarity index 98% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment_.java index 2b6d3332..c46b3e09 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment_.java @@ -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.Metadata.TYPE_ATTENDANCE; -public class AttendanceFragment extends Fragment { +public class AttendanceFragment_ extends Fragment { private App app = null; private MainActivity activity = null; @@ -278,11 +278,12 @@ public class AttendanceFragment extends Fragment { b.attendanceView.setVisibility(View.VISIBLE); b.attendanceNoData.setVisibility(View.GONE); if ((adapter = (AttendanceAdapter) b.attendanceView.getAdapter()) != null) { - adapter.attendanceList = filteredList; + //adapter.setItems(filteredList); adapter.notifyDataSetChanged(); } else { - adapter = new AttendanceAdapter(getContext(), filteredList); + //adapter = new AttendanceAdapter(activity, true, null); + //adapter.setItems(filteredList); b.attendanceView.setAdapter(adapter); } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt new file mode 100644 index 00000000..5f5da8ed --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt @@ -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): MutableList { + 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() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt new file mode 100644 index 00000000..829f03a0 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt @@ -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): MutableList { + 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() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceView.kt new file mode 100644 index 00000000..dfe20eda --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceView.kt @@ -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) + } + } +} + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceCount.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceCount.kt new file mode 100644 index 00000000..c2595885 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceCount.kt @@ -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 +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceDayRange.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceDayRange.kt new file mode 100644 index 00000000..e993d785 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceDayRange.kt @@ -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 = mutableListOf() +) : ExpandableItemModel(items) { + override var level = 1 + + var lastAddedDate = 0L + + var hasUnseen: Boolean = false + get() = field || items.any { it.baseType != Attendance.TYPE_PRESENT && !it.seen } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceEmpty.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceEmpty.kt new file mode 100644 index 00000000..46877f63 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceEmpty.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-5-4. + */ + +package pl.szczodrzynski.edziennik.ui.modules.attendance.models + +class AttendanceEmpty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceMonth.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceMonth.kt new file mode 100644 index 00000000..e1fde3f2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceMonth.kt @@ -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 = mutableListOf() +) : ExpandableItemModel(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 = mapOf() + var percentage: Float = 0f +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceSubject.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceSubject.kt new file mode 100644 index 00000000..67332fc9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceSubject.kt @@ -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 = mutableListOf() +) : ExpandableItemModel(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 = mapOf() + var percentage: Float = 0f +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/AttendanceViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/AttendanceViewHolder.kt new file mode 100644 index 00000000..e67c68b7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/AttendanceViewHolder.kt @@ -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 { + 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) + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/DayRangeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/DayRangeViewHolder.kt new file mode 100644 index 00000000..11a5005c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/DayRangeViewHolder.kt @@ -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 { + 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) + }) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/EmptyViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/EmptyViewHolder.kt new file mode 100644 index 00000000..37fe635c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/EmptyViewHolder.kt @@ -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 { + companion object { + private const val TAG = "EmptyViewHolder" + } + + override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceEmpty, position: Int, adapter: AttendanceAdapter) { + + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/MonthViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/MonthViewHolder.kt new file mode 100644 index 00000000..0c46cdab --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/MonthViewHolder.kt @@ -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 { + 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) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/SubjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/SubjectViewHolder.kt new file mode 100644 index 00000000..d079a3a1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/SubjectViewHolder.kt @@ -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 { + 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) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/ExpandableItemModel.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/ExpandableItemModel.kt index c52ba310..20cb3ffc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/ExpandableItemModel.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/ExpandableItemModel.kt @@ -6,7 +6,7 @@ package pl.szczodrzynski.edziennik.ui.modules.grades.models import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter.Companion.STATE_CLOSED -abstract class ExpandableItemModel(val items: MutableList) { +abstract class ExpandableItemModel(open val items: MutableList) { open var level: Int = 3 var state: Int = STATE_CLOSED } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java index 3e3eac0e..45e457fe 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java @@ -52,6 +52,7 @@ import pl.szczodrzynski.edziennik.network.NetworkUtils; import pl.szczodrzynski.edziennik.sync.SyncWorker; import pl.szczodrzynski.edziennik.sync.UpdateWorker; 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.ProfileRemoveDialog; import pl.szczodrzynski.edziennik.ui.dialogs.sync.NotificationFilterDialog; @@ -885,6 +886,15 @@ public class SettingsNewFragment extends MaterialAboutFragment { .color(IconicsColor.colorInt(iconColor)) ).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( getString(R.string.settings_register_allow_registration_text), getString(R.string.settings_register_allow_registration_subtext), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AttendanceManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AttendanceManager.kt index 953d792c..e77e5fbd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AttendanceManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AttendanceManager.kt @@ -19,10 +19,13 @@ class AttendanceManager(val app: App) : CoroutineScope { override val coroutineContext: CoroutineContext get() = job + Dispatchers.Default + val useSymbols + get() = app.config.forProfile().attendance.useSymbols + fun getTypeShort(baseType: Int): String { return when (baseType) { Attendance.TYPE_PRESENT -> "ob" - Attendance.TYPE_PRESENT_CUSTOM -> "ob?" + Attendance.TYPE_PRESENT_CUSTOM -> " " Attendance.TYPE_ABSENT -> "nb" Attendance.TYPE_ABSENT_EXCUSED -> "u" 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) + } + } + /* _ _ _____ _____ _ __ _ | | | |_ _| / ____| (_)/ _(_) | | | | | | | (___ _ __ ___ ___ _| |_ _ ___ diff --git a/app/src/main/res/layout/attendance_config_dialog.xml b/app/src/main/res/layout/attendance_config_dialog.xml new file mode 100644 index 00000000..b31edc72 --- /dev/null +++ b/app/src/main/res/layout/attendance_config_dialog.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/attendance_fragment.xml b/app/src/main/res/layout/attendance_fragment.xml new file mode 100644 index 00000000..d83601f0 --- /dev/null +++ b/app/src/main/res/layout/attendance_fragment.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/attendance_item_attendance.xml b/app/src/main/res/layout/attendance_item_attendance.xml new file mode 100644 index 00000000..5362b4a6 --- /dev/null +++ b/app/src/main/res/layout/attendance_item_attendance.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/attendance_item_container.xml b/app/src/main/res/layout/attendance_item_container.xml new file mode 100644 index 00000000..4d33192f --- /dev/null +++ b/app/src/main/res/layout/attendance_item_container.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/attendance_item_container_bar.xml b/app/src/main/res/layout/attendance_item_container_bar.xml new file mode 100644 index 00000000..63ff39ed --- /dev/null +++ b/app/src/main/res/layout/attendance_item_container_bar.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/attendance_item_empty.xml b/app/src/main/res/layout/attendance_item_empty.xml new file mode 100644 index 00000000..2d868178 --- /dev/null +++ b/app/src/main/res/layout/attendance_item_empty.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/attendance_list_fragment.xml b/app/src/main/res/layout/attendance_list_fragment.xml new file mode 100644 index 00000000..0d6e2abe --- /dev/null +++ b/app/src/main/res/layout/attendance_list_fragment.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/attendance_summary_fragment.xml b/app/src/main/res/layout/attendance_summary_fragment.xml new file mode 100644 index 00000000..d3b1404e --- /dev/null +++ b/app/src/main/res/layout/attendance_summary_fragment.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/grades_item_empty.xml b/app/src/main/res/layout/grades_item_empty.xml index 3ae82483..085eed62 100644 --- a/app/src/main/res/layout/grades_item_empty.xml +++ b/app/src/main/res/layout/grades_item_empty.xml @@ -16,6 +16,6 @@ android:textSize="18sp" android:textColor="?android:textColorSecondary" android:textStyle="italic" - android:text="Nie ma ocen w tym semestrze."/> + android:text="@string/grades_empty_text"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df773c90..ef8e64bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,7 +69,7 @@ Nieobecności: W tym nieusprawiedliwione: Spóźnienia: - Brak nieobecności. + Nie masz żadnych nieobecności. Obecności: Zwolnienia: Wszystkie przedmioty @@ -1288,4 +1288,19 @@ Połączenie sieciowe Dodaj nowego ucznia Zaloguj konto ucznia/rodzica w aplikacji + lekcja %d + Ustawienia frekwencji + Dni + Miesiące + Podsumowanie + Lista + Obecność w tym okresie: %.2f%% + %.2f%% + Nie ma ocen w tym semestrze. + Nie ma tutaj żadnych nieobecności. + Konfiguracja frekwencji + Używaj symboli i kolorów wg dziennika + Grupuj kolejne dni na liście + Wyświetlaj obecność w widoku miesięcy + Widoczne po rozwinięciu listy diff --git a/build.gradle b/build.gradle index 6143c28c..ffac6f70 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { ] versions = [ - gradleAndroid : "4.0.0-beta03", + gradleAndroid : '4.0.0-beta05', kotlin : ext.kotlin_version, ktx : "1.2.0",