Skip to content

Commit aeb5c93

Browse files
Okuro3499dogi
andauthored
courses: smoother filter selecting (fixes #13493) (#13494)
Co-authored-by: dogi <dogi@users.noreply.github.com>
1 parent 9f0e392 commit aeb5c93

4 files changed

Lines changed: 434 additions & 372 deletions

File tree

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ android {
1212
applicationId "org.ole.planet.myplanet"
1313
minSdk = 26
1414
targetSdk = 36
15-
versionCode = 5588
16-
versionName = "0.55.88"
15+
versionCode = 5589
16+
versionName = "0.55.89"
1717
ndkVersion = '26.3.11579264'
1818
vectorDrawables.useSupportLibrary = true
1919
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package org.ole.planet.myplanet.ui.courses
2+
3+
import android.text.Editable
4+
import android.text.TextWatcher
5+
import android.view.View
6+
import android.widget.AdapterView
7+
import android.widget.ArrayAdapter
8+
import android.widget.EditText
9+
import android.widget.Spinner
10+
import android.widget.TextView
11+
import kotlinx.coroutines.CoroutineScope
12+
import kotlinx.coroutines.Job
13+
import kotlinx.coroutines.delay
14+
import kotlinx.coroutines.launch
15+
import org.ole.planet.myplanet.R
16+
import org.ole.planet.myplanet.model.RealmTag
17+
18+
data class FilterState(
19+
val searchText: String,
20+
val grade: String,
21+
val subject: String,
22+
val tagNames: List<String>
23+
) {
24+
val isActive: Boolean
25+
get() = searchText.isNotEmpty() || grade.isNotEmpty() || subject.isNotEmpty() || tagNames.isNotEmpty()
26+
}
27+
28+
class CourseFilterController(
29+
private val rootView: View,
30+
private val scope: CoroutineScope,
31+
private val onFilterChanged: (FilterState) -> Unit,
32+
private val onScrollToTop: () -> Unit
33+
) {
34+
private lateinit var etSearch: EditText
35+
private lateinit var spnGrade: Spinner
36+
private lateinit var spnSubject: Spinner
37+
private lateinit var tvSelected: TextView
38+
val searchTags: MutableList<RealmTag> = ArrayList()
39+
private var searchJob: Job? = null
40+
private var searchTextWatcher: TextWatcher? = null
41+
42+
fun setup() {
43+
etSearch = rootView.findViewById(R.id.et_search)
44+
spnGrade = rootView.findViewById(R.id.spn_grade)
45+
spnSubject = rootView.findViewById(R.id.spn_subject)
46+
tvSelected = rootView.findViewById(R.id.tv_selected)
47+
setupSpinners()
48+
setupSearchWatcher()
49+
setupClearTagsButton()
50+
}
51+
52+
private fun setupSpinners() {
53+
val ctx = rootView.context
54+
val gradeAdapter = ArrayAdapter.createFromResource(ctx, R.array.grade_level, R.layout.spinner_item)
55+
gradeAdapter.setDropDownViewResource(R.layout.custom_simple_list_item_1)
56+
spnGrade.adapter = gradeAdapter
57+
58+
val subjectAdapter = ArrayAdapter.createFromResource(ctx, R.array.subject_level, R.layout.spinner_item)
59+
subjectAdapter.setDropDownViewResource(R.layout.custom_simple_list_item_1)
60+
spnSubject.adapter = subjectAdapter
61+
62+
val spinnerListener = object : AdapterView.OnItemSelectedListener {
63+
override fun onItemSelected(parent: AdapterView<*>?, view: View?, i: Int, l: Long) {
64+
if (view == null) return
65+
onFilterChanged(currentState())
66+
onScrollToTop()
67+
}
68+
override fun onNothingSelected(parent: AdapterView<*>?) {}
69+
}
70+
spnGrade.onItemSelectedListener = spinnerListener
71+
spnSubject.onItemSelectedListener = spinnerListener
72+
}
73+
74+
private fun setupSearchWatcher() {
75+
searchTextWatcher = object : TextWatcher {
76+
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
77+
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
78+
if (!etSearch.isFocused) return
79+
searchJob?.cancel()
80+
searchJob = scope.launch {
81+
delay(300)
82+
onFilterChanged(currentState())
83+
}
84+
}
85+
override fun afterTextChanged(s: Editable) {}
86+
}
87+
etSearch.addTextChangedListener(searchTextWatcher)
88+
}
89+
90+
private fun setupClearTagsButton() {
91+
rootView.findViewById<View>(R.id.btn_clear_tags).setOnClickListener { clearAll() }
92+
}
93+
94+
fun addTag(tag: RealmTag) {
95+
if (!searchTags.any { it.name == tag.name }) searchTags.add(tag)
96+
onFilterChanged(currentState())
97+
refreshTagText()
98+
onScrollToTop()
99+
}
100+
101+
fun setTags(list: List<RealmTag>) {
102+
searchTags.clear()
103+
list.forEach { tag -> if (!searchTags.any { it.name == tag.name }) searchTags.add(tag) }
104+
onFilterChanged(currentState())
105+
onScrollToTop()
106+
}
107+
108+
fun setSingleTag(tag: RealmTag) {
109+
searchTags.clear()
110+
searchTags.add(tag)
111+
tvSelected.text = tvSelected.context.getString(R.string.tag_selected, tag.name)
112+
onFilterChanged(currentState())
113+
onScrollToTop()
114+
}
115+
116+
fun clearAll() {
117+
searchTags.clear()
118+
etSearch.setText("")
119+
tvSelected.text = ""
120+
spnGrade.setSelection(0)
121+
spnSubject.setSelection(0)
122+
onFilterChanged(currentState())
123+
onScrollToTop()
124+
}
125+
126+
fun filterApplied(): Boolean = currentState().isActive
127+
128+
fun currentState(): FilterState {
129+
val grade = spnGrade.selectedItem?.toString()?.takeIf { it != "All" } ?: ""
130+
val subject = spnSubject.selectedItem?.toString()?.takeIf { it != "All" } ?: ""
131+
return FilterState(
132+
searchText = etSearch.text.toString().trim(),
133+
grade = grade,
134+
subject = subject,
135+
tagNames = searchTags.mapNotNull { it.name }
136+
)
137+
}
138+
139+
fun setListVisible(visible: Boolean) {
140+
val visibility = if (visible) View.VISIBLE else View.GONE
141+
etSearch.visibility = visibility
142+
rootView.findViewById<View>(R.id.filter).visibility = visibility
143+
if (!visible) tvSelected.visibility = View.GONE
144+
}
145+
146+
private fun refreshTagText() {
147+
tvSelected.text = searchTags.joinToString(
148+
separator = ",",
149+
prefix = tvSelected.context.getString(R.string.selected)
150+
) { it.name.orEmpty() }
151+
}
152+
153+
fun detach() {
154+
searchTextWatcher?.let { etSearch.removeTextChangedListener(it) }
155+
searchTextWatcher = null
156+
searchJob?.cancel()
157+
}
158+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package org.ole.planet.myplanet.ui.courses
2+
3+
import android.app.AlertDialog
4+
import android.content.DialogInterface
5+
import android.view.View
6+
import android.widget.Button
7+
import android.widget.CheckBox
8+
import android.widget.TextView
9+
import androidx.appcompat.view.ContextThemeWrapper
10+
import org.ole.planet.myplanet.R
11+
12+
class CourseSelectionController(
13+
private val rootView: View,
14+
private val isMyCourseLib: Boolean,
15+
private val isGuest: Boolean,
16+
private val onRemoveConfirmed: () -> Unit,
17+
private val onArchiveConfirmed: () -> Unit,
18+
private val onAddToLib: () -> Unit,
19+
private val onSelectAllToggled: (checked: Boolean) -> Unit
20+
) {
21+
lateinit var selectAll: CheckBox
22+
private set
23+
private lateinit var tvAddToLib: TextView
24+
private lateinit var btnRemove: Button
25+
private lateinit var btnArchive: Button
26+
private var isUpdatingSelectAllState = false
27+
private var currentSelectedCount = 0
28+
29+
fun setup() {
30+
selectAll = rootView.findViewById(R.id.selectAllCourse)
31+
tvAddToLib = rootView.findViewById(R.id.tv_add)
32+
btnRemove = rootView.findViewById(R.id.btn_remove)
33+
btnArchive = rootView.findViewById(R.id.btn_archive)
34+
35+
if (isGuest) {
36+
tvAddToLib.visibility = View.GONE
37+
btnRemove.visibility = View.GONE
38+
btnArchive.visibility = View.GONE
39+
selectAll.visibility = View.GONE
40+
return
41+
}
42+
43+
tvAddToLib.setOnClickListener { onAddToLib() }
44+
45+
btnRemove.setOnClickListener {
46+
showConfirmDialog(
47+
if (currentSelectedCount == 1) R.string.are_you_sure_you_want_to_leave_this_course
48+
else R.string.are_you_sure_you_want_to_leave_these_courses,
49+
onRemoveConfirmed
50+
)
51+
}
52+
53+
btnArchive.setOnClickListener {
54+
showConfirmDialog(
55+
if (currentSelectedCount == 1) R.string.are_you_sure_you_want_to_archive_this_course
56+
else R.string.are_you_sure_you_want_to_archive_these_courses,
57+
onArchiveConfirmed
58+
)
59+
}
60+
61+
selectAll.setOnCheckedChangeListener { _, isChecked ->
62+
if (isUpdatingSelectAllState) return@setOnCheckedChangeListener
63+
onSelectAllToggled(isChecked)
64+
selectAll.text = rootView.context.getString(
65+
if (isChecked) R.string.unselect_all else R.string.select_all
66+
)
67+
}
68+
}
69+
70+
fun onSelectionChanged(selectedCount: Int, areAllSelected: Boolean) {
71+
currentSelectedCount = selectedCount
72+
val hasSelection = selectedCount > 0
73+
btnArchive.isEnabled = hasSelection
74+
btnRemove.isEnabled = hasSelection
75+
tvAddToLib.isEnabled = hasSelection
76+
refreshActionVisibility()
77+
syncSelectAll(areAllSelected)
78+
}
79+
80+
fun onListChanged(isEmpty: Boolean, hasSelectableItems: Boolean) {
81+
if (isEmpty) {
82+
selectAll.visibility = View.GONE
83+
tvAddToLib.visibility = View.GONE
84+
btnRemove.visibility = View.GONE
85+
btnArchive.visibility = View.GONE
86+
} else if (!isGuest) {
87+
selectAll.visibility = if (hasSelectableItems) View.VISIBLE else View.GONE
88+
}
89+
}
90+
91+
fun clearAll(adapter: CoursesAdapter?) {
92+
adapter?.selectAllItems(false)
93+
currentSelectedCount = 0
94+
syncSelectAll(false)
95+
refreshActionVisibility()
96+
}
97+
98+
private fun refreshActionVisibility() {
99+
if (currentSelectedCount > 0) {
100+
if (isMyCourseLib) {
101+
btnRemove.visibility = View.VISIBLE
102+
btnArchive.visibility = View.VISIBLE
103+
} else {
104+
tvAddToLib.visibility = View.VISIBLE
105+
}
106+
} else {
107+
if (isMyCourseLib) {
108+
btnRemove.visibility = View.GONE
109+
btnArchive.visibility = View.GONE
110+
} else {
111+
tvAddToLib.visibility = View.GONE
112+
}
113+
}
114+
}
115+
116+
private fun syncSelectAll(allSelected: Boolean) {
117+
isUpdatingSelectAllState = true
118+
selectAll.isChecked = allSelected
119+
selectAll.text = rootView.context.getString(
120+
if (allSelected) R.string.unselect_all else R.string.select_all
121+
)
122+
isUpdatingSelectAllState = false
123+
}
124+
125+
private fun showConfirmDialog(messageRes: Int, onConfirmed: () -> Unit) {
126+
AlertDialog.Builder(ContextThemeWrapper(rootView.context, R.style.CustomAlertDialog))
127+
.setMessage(messageRes)
128+
.setPositiveButton(R.string.yes) { _: DialogInterface, _: Int -> onConfirmed() }
129+
.setNegativeButton(R.string.no, null)
130+
.show()
131+
}
132+
}

0 commit comments

Comments
 (0)