Allow expanding multiple subject' grades at once (#1584)

This commit is contained in:
Michael 2021-10-24 01:23:36 +02:00 committed by GitHub
parent 94fd303f8e
commit 0f800b61f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 187 additions and 65 deletions

View File

@ -193,7 +193,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration38(),
Migration39(),
Migration40(),
Migration41(),
Migration41(sharedPrefProvider),
Migration42()
)

View File

@ -2,10 +2,20 @@ package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
class Migration41 : Migration(40, 41) {
class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migration(40, 41) {
override fun migrate(database: SupportSQLiteDatabase) {
migrateSharedPreferences()
database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0")
}
private fun migrateSharedPreferences() {
if (sharedPrefProvider.getBoolean("pref_key_expand_grade", false)) {
sharedPrefProvider.putString("pref_key_expand_grade_mode", GradeExpandMode.ALWAYS_EXPANDED.value)
}
sharedPrefProvider.delete("pref_key_expand_grade")
}
}

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.toLocalDate
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode
import io.github.wulkanowy.utils.toLocalDateTime
import io.github.wulkanowy.utils.toTimestamp
@ -19,6 +20,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.lang.ClassCastException
import java.lang.IllegalStateException
import java.time.LocalDate
import java.time.LocalDateTime
import javax.inject.Inject
@ -56,8 +59,13 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_grade_average_force_calc
)
val isGradeExpandable: Boolean
get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade)
val gradeExpandMode: GradeExpandMode
get() = GradeExpandMode.getByValue(
getString(
R.string.pref_key_expand_grade_mode,
R.string.pref_default_expand_grade_mode
)
)
val showAllSubjectsOnStatisticsList: Boolean
get() = getBoolean(
@ -265,6 +273,9 @@ class PreferencesRepository @Inject constructor(
private fun getBoolean(id: String, default: Int) =
sharedPref.getBoolean(id, context.resources.getBoolean(default))
private fun getBoolean(id: Int, default: Boolean) =
sharedPref.getBoolean(context.getString(id), default)
private companion object {
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.ui.modules.grade
enum class GradeExpandMode(val value: String) {
ONE("one"), UNLIMITED("any"), ALWAYS_EXPANDED("always");
companion object {
fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE
}
}

View File

@ -5,6 +5,7 @@ import android.content.res.Resources
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
@ -13,9 +14,11 @@ import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding
import io.github.wulkanowy.databinding.ItemGradeDetailsBinding
import io.github.wulkanowy.ui.base.BaseExpandableAdapter
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
import io.github.wulkanowy.utils.getBackgroundColor
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber
import java.util.BitSet
import javax.inject.Inject
class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<RecyclerView.ViewHolder>() {
@ -24,19 +27,20 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
private var items = mutableListOf<GradeDetailsItem>()
private var expandedPosition = NO_POSITION
private val expandedPositions = BitSet(items.size)
private var isExpandable = false
private var expandMode = GradeExpandMode.ONE
var onClickListener: (Grade, position: Int) -> Unit = { _, _ -> }
var colorTheme = ""
fun setDataItems(data: List<GradeDetailsItem>, isExpanded: Boolean = isExpandable) {
fun setDataItems(data: List<GradeDetailsItem>, expandMode: GradeExpandMode = this.expandMode) {
headers = data.filter { it.viewType == ViewType.HEADER }.toMutableList()
items = if (isExpanded) headers else data.toMutableList()
isExpandable = isExpanded
expandedPosition = NO_POSITION
items =
(if (expandMode != GradeExpandMode.ALWAYS_EXPANDED) headers else data).toMutableList()
this.expandMode = expandMode
expandedPositions.clear()
}
fun updateDetailsItem(position: Int, grade: Grade) {
@ -48,7 +52,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
val candidates = headers.filter { (it.value as GradeDetailsHeader).subject == subject }
if (candidates.size > 1) {
Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates")
Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPositions. Items: $candidates")
}
return candidates.first()
@ -64,9 +68,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
}
fun collapseAll() {
if (expandedPosition != -1) {
refreshList(headers)
expandedPosition = NO_POSITION
if (!expandedPositions.isEmpty) {
refreshList(headers.toMutableList())
expandedPositions.clear()
}
}
@ -86,8 +90,12 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.HEADER.id -> HeaderViewHolder(HeaderGradeDetailsBinding.inflate(inflater, parent, false))
ViewType.ITEM.id -> ItemViewHolder(ItemGradeDetailsBinding.inflate(inflater, parent, false))
ViewType.HEADER.id -> HeaderViewHolder(
HeaderGradeDetailsBinding.inflate(inflater, parent, false)
)
ViewType.ITEM.id -> ItemViewHolder(
ItemGradeDetailsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
@ -106,46 +114,91 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
}
}
private fun bindHeaderViewHolder(holder: HeaderViewHolder, header: GradeDetailsHeader, position: Int) {
val headerPosition = headers.indexOf(items[position])
val adapterPosition = holder.bindingAdapterPosition
private fun bindHeaderViewHolder(
holder: HeaderViewHolder,
header: GradeDetailsHeader,
position: Int
) {
val context = holder.binding.root.context
val item = items[position]
val headerPosition = headers.indexOf(item)
with(holder.binding) {
gradeHeaderDivider.visibility = if (adapterPosition == 0) View.GONE else View.VISIBLE
gradeHeaderDivider.isVisible = holder.bindingAdapterPosition != 0
with(gradeHeaderSubject) {
text = header.subject
maxLines = if (headerPosition == expandedPosition) 2 else 1
maxLines = if (expandedPositions[headerPosition]) 2 else 1
}
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
gradeHeaderPointsSum.text = root.context.getString(R.string.grade_points_sum, header.pointsSum)
gradeHeaderPointsSum.visibility = if (!header.pointsSum.isNullOrEmpty()) View.VISIBLE else View.GONE
gradeHeaderNumber.text = root.context.resources.getQuantityString(R.plurals.grade_number_item, header.grades.size, header.grades.size)
gradeHeaderNote.visibility = if (header.newGrades > 0) View.VISIBLE else View.GONE
if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10)
gradeHeaderPointsSum.text =
context.getString(R.string.grade_points_sum, header.pointsSum)
gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty()
gradeHeaderNumber.text = context.resources.getQuantityString(
R.plurals.grade_number_item,
header.grades.size,
header.grades.size
)
gradeHeaderNote.isVisible = header.newGrades > 0
gradeHeaderContainer.isEnabled = isExpandable
if (header.newGrades > 0) {
gradeHeaderNote.text = header.newGrades.toString()
}
gradeHeaderContainer.isEnabled = expandMode != GradeExpandMode.ALWAYS_EXPANDED
gradeHeaderContainer.setOnClickListener {
expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition
if (expandedPosition != NO_POSITION) {
refreshList(headers.toMutableList().apply {
addAll(headerPosition + 1, header.grades)
})
scrollToHeaderWithSubItems(headerPosition, header.grades.size)
} else {
refreshList(headers)
}
expandGradeHeader(headerPosition, header, holder)
}
}
}
private fun formatAverage(average: Double?, resources: Resources): String {
return if (average == null || average == .0) resources.getString(R.string.grade_no_average)
else resources.getString(R.string.grade_average, average)
private fun expandGradeHeader(
headerPosition: Int,
header: GradeDetailsHeader,
holder: HeaderViewHolder
) {
if (expandMode == GradeExpandMode.ONE) {
val isHeaderExpanded = expandedPositions[headerPosition]
expandedPositions.clear()
if (!isHeaderExpanded) {
val updatedItemList = headers.toMutableList()
.apply { addAll(headerPosition + 1, header.grades) }
expandedPositions.set(headerPosition)
refreshList(updatedItemList)
scrollToHeaderWithSubItems(headerPosition, header.grades.size)
} else {
refreshList(headers.toMutableList())
}
} else if (expandMode == GradeExpandMode.UNLIMITED) {
val headerAdapterPosition = holder.bindingAdapterPosition
val isHeaderExpanded = expandedPositions[headerPosition]
expandedPositions.flip(headerPosition)
if (!isHeaderExpanded) {
val updatedList = items.toMutableList()
.apply { addAll(headerAdapterPosition + 1, header.grades) }
refreshList(updatedList)
scrollToHeaderWithSubItems(headerAdapterPosition, header.grades.size)
} else {
val startPosition = headerAdapterPosition + 1
val updatedList = items.toMutableList()
.apply {
subList(startPosition, startPosition + header.grades.size).clear()
}
refreshList(updatedList)
}
}
}
@SuppressLint("SetTextI18n")
private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) {
val context = holder.binding.root.context
with(holder.binding) {
gradeItemValue.run {
text = grade.entry
@ -154,26 +207,37 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
gradeItemDescription.text = when {
grade.description.isNotBlank() -> grade.description
grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol
else -> root.context.getString(R.string.all_no_description)
else -> context.getString(R.string.all_no_description)
}
gradeItemDate.text = grade.date.toFormattedString()
gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}"
gradeItemWeight.text = "${context.getString(R.string.grade_weight)}: ${grade.weight}"
gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE
root.setOnClickListener {
holder.bindingAdapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) }
holder.bindingAdapterPosition.let {
if (it != NO_POSITION) onClickListener(grade, it)
}
}
}
}
private fun formatAverage(average: Double?, resources: Resources) =
if (average == null || average == .0) {
resources.getString(R.string.grade_no_average)
} else {
resources.getString(R.string.grade_average, average)
}
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
RecyclerView.ViewHolder(binding.root)
private class ItemViewHolder(val binding: ItemGradeDetailsBinding) :
RecyclerView.ViewHolder(binding.root)
class GradeDetailsDiffUtil(private val old: List<GradeDetailsItem>, private val new: List<GradeDetailsItem>) :
DiffUtil.Callback() {
private class GradeDetailsDiffUtil(
private val old: List<GradeDetailsItem>,
private val new: List<GradeDetailsItem>
) : DiffUtil.Callback() {
override fun getOldListSize() = old.size

View File

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
import io.github.wulkanowy.databinding.FragmentGradeDetailsBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
@ -79,10 +80,10 @@ class GradeDetailsFragment :
else false
}
override fun updateData(data: List<GradeDetailsItem>, isGradeExpandable: Boolean, gradeColorTheme: String) {
override fun updateData(data: List<GradeDetailsItem>, expandMode: GradeExpandMode, gradeColorTheme: String) {
with(gradeDetailsAdapter) {
colorTheme = gradeColorTheme
setDataItems(data, isGradeExpandable)
setDataItems(data, expandMode)
notifyDataSetChanged()
}
}

View File

@ -9,6 +9,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.ALPHABETIC
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE
import io.github.wulkanowy.ui.modules.grade.GradeSubject
@ -113,7 +114,7 @@ class GradeDetailsPresenter @Inject constructor(
fun onParentViewReselected() {
view?.run {
if (!isViewEmpty) {
if (preferencesRepository.isGradeExpandable) collapseAllItems()
if (preferencesRepository.gradeExpandMode != GradeExpandMode.ALWAYS_EXPANDED) collapseAllItems()
scrollToStart()
}
}
@ -157,7 +158,7 @@ class GradeDetailsPresenter @Inject constructor(
showContent(true)
updateData(
data = items,
isGradeExpandable = preferencesRepository.isGradeExpandable,
expandMode = preferencesRepository.gradeExpandMode,
gradeColorTheme = preferencesRepository.gradeColorTheme
)
notifyParentDataLoaded(semesterId)
@ -175,7 +176,7 @@ class GradeDetailsPresenter @Inject constructor(
showContent(items.isNotEmpty())
updateData(
data = items,
isGradeExpandable = preferencesRepository.isGradeExpandable,
expandMode = preferencesRepository.gradeExpandMode,
gradeColorTheme = preferencesRepository.gradeColorTheme
)
}
@ -235,14 +236,24 @@ class GradeDetailsPresenter @Inject constructor(
.sortedByDescending { it.date }
.map { GradeDetailsItem(it, ViewType.ITEM) }
listOf(GradeDetailsItem(GradeDetailsHeader(
subject = subject,
average = average,
pointsSum = points,
grades = subItems
).apply {
newGrades = grades.filter { grade -> !grade.isRead }.size
}, ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems
val gradeDetailsItems = listOf(
GradeDetailsItem(
GradeDetailsHeader(
subject = subject,
average = average,
pointsSum = points,
grades = subItems
).apply {
newGrades = grades.filter { grade -> !grade.isRead }.size
}, ViewType.HEADER
)
)
if (preferencesRepository.gradeExpandMode == GradeExpandMode.ALWAYS_EXPANDED) {
gradeDetailsItems + subItems
} else {
gradeDetailsItems
}
}.flatten()
}

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.grade.details
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
import io.github.wulkanowy.ui.base.BaseView
interface GradeDetailsView : BaseView {
@ -9,7 +10,7 @@ interface GradeDetailsView : BaseView {
fun initView()
fun updateData(data: List<GradeDetailsItem>, isGradeExpandable: Boolean, gradeColorTheme: String)
fun updateData(data: List<GradeDetailsItem>, expandMode: GradeExpandMode, gradeColorTheme: String)
fun updateItem(item: Grade, position: Int)

View File

@ -4,7 +4,7 @@
<bool name="pref_default_attendance_present">true</bool>
<string name="pref_default_grade_average_mode">only_one_semester</string>
<bool name="pref_default_grade_average_force_calc">false</bool>
<bool name="pref_default_expand_grade">false</bool>
<string name="pref_default_expand_grade_mode">one</string>
<bool name="pref_default_grade_statistics_list">false</bool>
<string name="pref_default_app_theme">light</string>
<string name="pref_default_grade_color_scheme">vulcan</string>

View File

@ -5,7 +5,8 @@
<string name="pref_key_app_theme">app_theme</string>
<string name="pref_key_dashboard_tiles">dashboard_tiles</string>
<string name="pref_key_grade_color_scheme">grade_color_scheme</string>
<string name="pref_key_expand_grade">expand_grade</string>
<string name="pref_key_expand_grade">expand_grade</string> <!-- replaced by expand_grade_mode -->
<string name="pref_key_expand_grade_mode">expand_grade_mode</string>
<string name="pref_key_grade_average_mode">grade_average_mode</string>
<string name="pref_key_grade_average_force_calc">grade_average_always_calc</string>
<string name="pref_key_grade_statistics_list">grade_statistics_list</string>

View File

@ -99,6 +99,17 @@
<item>grade_color</item>
</string-array>
<string-array name="default_expand_grade_entries">
<item>Up to 1 at once</item>
<item>Always expanded</item>
<item>Unlimited expansions</item>
</string-array>
<string-array name="default_expand_grade_values" translatable="false">
<item>one</item>
<item>always</item>
<item>any</item>
</string-array>
<string-array name="grade_average_mode_entries">
<item>Average of grades only from selected semester</item>
<item>Average of averages from both semesters</item>

View File

@ -50,11 +50,14 @@
app:iconSpaceReserved="false"
app:key="@string/pref_key_grade_color_scheme"
app:title="@string/pref_view_grade_color_scheme" />
<SwitchPreferenceCompat
app:defaultValue="@bool/pref_default_expand_grade"
<ListPreference
app:defaultValue="@string/pref_default_expand_grade_mode"
app:entries="@array/default_expand_grade_entries"
app:entryValues="@array/default_expand_grade_values"
app:iconSpaceReserved="false"
app:key="@string/pref_key_expand_grade"
app:title="@string/pref_view_expand_grade" />
app:key="@string/pref_key_expand_grade_mode"
app:title="@string/pref_view_expand_grade"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:defaultValue="@bool/pref_default_subjects_without_grades"
app:iconSpaceReserved="false"