Skip to content

Commit b64c1d3

Browse files
committed
add filtering to categories screen
1 parent 0de31ab commit b64c1d3

File tree

54 files changed

+732
-245
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+732
-245
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
package com.example.util.simpletimetracker.core.dialog
22

3+
import com.example.util.simpletimetracker.domain.statistics.model.ChartFilterType
4+
35
interface ChartFilterDialogListener {
46

7+
fun onChartFilterDataSelected(
8+
chartFilterType: ChartFilterType,
9+
dataIds: List<Long>,
10+
)
11+
512
fun onChartFilterDialogDismissed()
613
}

core/src/main/java/com/example/util/simpletimetracker/core/model/OptionsListItem.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ sealed interface OptionsListItem : OptionsListViewData.Id {
2020
data object Filter : StatisticsDetailContainer
2121
data object Compare : StatisticsDetailContainer
2222
}
23+
24+
sealed interface Categories : OptionsListItem {
25+
data object Filter : Categories
26+
data object EnabledSearch : Categories
27+
}
2328
}

data_local/src/main/java/com/example/util/simpletimetracker/data_local/backup/BackupPrefsRepo.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Compani
3535
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_INACTIVITY_REMINDER_DURATION
3636
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_INACTIVITY_REMINDER_RECURRENT
3737
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_IS_ACTIVITY_FILTERS_COLLAPSED
38+
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_IS_CATEGORIES_SEARCH_ENABLED
3839
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_IS_NAV_BAR_AT_THE_BOTTOM
3940
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_KEEP_SCREEN_ON
4041
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_KEEP_STATISTICS_RANGE
@@ -218,6 +219,7 @@ class BackupPrefsRepo @Inject constructor(
218219
PrefsProcessor(KEY_WIDGET_TRANSPARENCY_PERCENT, ::widgetBackgroundTransparencyPercent),
219220
PrefsProcessor(KEY_DEFAULT_TYPES_HIDDEN, ::defaultTypesHidden),
220221
PrefsProcessor(KEY_IS_NAV_BAR_AT_THE_BOTTOM, ::isNavBarAtTheBottom),
222+
PrefsProcessor(KEY_IS_CATEGORIES_SEARCH_ENABLED, ::isCategoriesSearchEnabled),
221223
)
222224
}
223225

data_local/src/main/java/com/example/util/simpletimetracker/data_local/prefs/PrefsRepoImpl.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,10 @@ class PrefsRepoImpl @Inject constructor(
384384
KEY_IS_NAV_BAR_AT_THE_BOTTOM, false,
385385
)
386386

387+
override var isCategoriesSearchEnabled: Boolean by prefs.delegate(
388+
KEY_IS_CATEGORIES_SEARCH_ENABLED, false,
389+
)
390+
387391
override fun setWidget(widgetId: Int, recordType: Long) {
388392
val key = KEY_WIDGET + widgetId
389393
logPrefsDataAccess("set $key")
@@ -640,6 +644,7 @@ class PrefsRepoImpl @Inject constructor(
640644
const val KEY_WIDGET_TRANSPARENCY_PERCENT = "widgetTransparencyPercent"
641645
const val KEY_DEFAULT_TYPES_HIDDEN = "defaultTypesHidden"
642646
const val KEY_IS_NAV_BAR_AT_THE_BOTTOM = "isNavBarAtTheBottom"
647+
const val KEY_IS_CATEGORIES_SEARCH_ENABLED = "isCategoriesSearchEnabled"
643648
const val KEY_CARD_ORDER_MANUAL = "cardOrderManual"
644649
const val KEY_CATEGORY_ORDER_MANUAL = "categoryOrderManual"
645650
const val KEY_TAG_ORDER_MANUAL = "tagOrderManual"

domain/src/main/java/com/example/util/simpletimetracker/domain/prefs/interactor/PrefsInteractor.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,14 @@ class PrefsInteractor @Inject constructor(
859859
prefsRepo.isNavBarAtTheBottom = value
860860
}
861861

862+
suspend fun getIsCategoriesSearchEnabled(): Boolean = withContext(Dispatchers.IO) {
863+
prefsRepo.isCategoriesSearchEnabled
864+
}
865+
866+
suspend fun setIsCategoriesSearchEnabled(value: Boolean) = withContext(Dispatchers.IO) {
867+
prefsRepo.isCategoriesSearchEnabled = value
868+
}
869+
862870
suspend fun clear() = withContext(Dispatchers.IO) {
863871
prefsRepo.clear()
864872
}
@@ -940,7 +948,7 @@ class PrefsInteractor @Inject constructor(
940948
}
941949
}
942950

943-
fun mapToChartFilterType(data: Int): ChartFilterType {
951+
private fun mapToChartFilterType(data: Int): ChartFilterType {
944952
return when (data) {
945953
0 -> ChartFilterType.ACTIVITY
946954
1 -> ChartFilterType.CATEGORY
@@ -949,7 +957,7 @@ class PrefsInteractor @Inject constructor(
949957
}
950958
}
951959

952-
fun mapFromChartFilterType(data: ChartFilterType): Int {
960+
private fun mapFromChartFilterType(data: ChartFilterType): Int {
953961
return when (data) {
954962
ChartFilterType.ACTIVITY -> 0
955963
ChartFilterType.CATEGORY -> 1

domain/src/main/java/com/example/util/simpletimetracker/domain/prefs/repo/PrefsRepo.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ interface PrefsRepo {
166166

167167
var isNavBarAtTheBottom: Boolean
168168

169+
var isCategoriesSearchEnabled: Boolean
170+
169171
fun setWidget(widgetId: Int, recordType: Long)
170172

171173
fun getWidget(widgetId: Int): Long

domain/src/main/java/com/example/util/simpletimetracker/domain/recordTag/interactor/RecordTypeToDefaultTagInteractor.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.example.util.simpletimetracker.domain.recordTag.interactor
22

3+
import com.example.util.simpletimetracker.domain.recordTag.model.RecordTypeToDefaultTag
34
import com.example.util.simpletimetracker.domain.recordTag.repo.RecordTypeToDefaultTagRepo
45
import javax.inject.Inject
56

67
class RecordTypeToDefaultTagInteractor @Inject constructor(
78
private val repo: RecordTypeToDefaultTagRepo,
89
) {
910

11+
suspend fun getAll(): List<RecordTypeToDefaultTag> {
12+
return repo.getAll()
13+
}
14+
1015
suspend fun getTags(typeId: Long): Set<Long> {
1116
return repo.getTagIdsByType(typeId)
1217
}
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,69 @@
11
package com.example.util.simpletimetracker.feature_categories.interactor
22

3-
import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType
4-
import com.example.util.simpletimetracker.feature_base_adapter.divider.DividerViewData
53
import com.example.util.simpletimetracker.core.mapper.CategoryViewDataMapper
4+
import com.example.util.simpletimetracker.core.repo.ResourceRepo
65
import com.example.util.simpletimetracker.domain.category.interactor.CategoryInteractor
6+
import com.example.util.simpletimetracker.domain.category.interactor.RecordTypeCategoryInteractor
7+
import com.example.util.simpletimetracker.domain.category.model.Category
78
import com.example.util.simpletimetracker.domain.prefs.interactor.PrefsInteractor
89
import com.example.util.simpletimetracker.domain.recordTag.interactor.RecordTagInteractor
10+
import com.example.util.simpletimetracker.domain.recordTag.interactor.RecordTypeToDefaultTagInteractor
11+
import com.example.util.simpletimetracker.domain.recordTag.interactor.RecordTypeToTagInteractor
12+
import com.example.util.simpletimetracker.domain.recordTag.model.RecordTag
913
import com.example.util.simpletimetracker.domain.recordType.interactor.RecordTypeInteractor
14+
import com.example.util.simpletimetracker.domain.recordType.model.RecordType
15+
import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType
16+
import com.example.util.simpletimetracker.feature_base_adapter.divider.DividerViewData
17+
import com.example.util.simpletimetracker.feature_base_adapter.emptySpace.EmptySpaceViewData
18+
import com.example.util.simpletimetracker.feature_base_adapter.hint.HintViewData
19+
import com.example.util.simpletimetracker.feature_categories.R
1020
import com.example.util.simpletimetracker.feature_categories.viewData.CategoriesViewData
21+
import kotlinx.coroutines.Dispatchers
1122
import kotlinx.coroutines.async
1223
import kotlinx.coroutines.coroutineScope
24+
import kotlinx.coroutines.withContext
1325
import javax.inject.Inject
1426

1527
class CategoriesViewDataInteractor @Inject constructor(
28+
private val resourceRepo: ResourceRepo,
1629
private val prefsInteractor: PrefsInteractor,
1730
private val categoryInteractor: CategoryInteractor,
1831
private val recordTagInteractor: RecordTagInteractor,
1932
private val recordTypeInteractor: RecordTypeInteractor,
33+
private val recordTypeCategoryInteractor: RecordTypeCategoryInteractor,
34+
private val recordTypeToDefaultTagInteractor: RecordTypeToDefaultTagInteractor,
35+
private val recordTypeToTagInteractor: RecordTypeToTagInteractor,
2036
private val categoryViewDataMapper: CategoryViewDataMapper,
2137
) {
2238

23-
suspend fun getViewData(): CategoriesViewData = coroutineScope {
24-
val typeTags = async { getRecordTypeTagViewData() }
25-
val recordTags = async { getRecordTagViewData() }
39+
suspend fun getViewData(
40+
selectedTypeIds: List<Long>,
41+
searchEnabled: Boolean,
42+
searchText: String,
43+
navBarHeightDp: Int,
44+
): CategoriesViewData = coroutineScope {
45+
val isSearching: Boolean = searchEnabled && searchText.isNotEmpty()
46+
47+
val typeTags = async {
48+
getCategoriesViewData(
49+
selectedTypeIds = selectedTypeIds,
50+
searchText = searchText,
51+
isSearching = isSearching,
52+
)
53+
}
54+
val recordTags = async {
55+
getRecordTagViewData(
56+
selectedTypeIds = selectedTypeIds,
57+
searchText = searchText,
58+
isSearching = isSearching,
59+
)
60+
}
2661

2762
val items = typeTags.await().items +
2863
DividerViewData(1) +
29-
recordTags.await().items
64+
recordTags.await().items +
65+
getBottomEmptySpace(navBarHeightDp)
66+
3067
val showHint = typeTags.await().showHint ||
3168
recordTags.await().showHint
3269

@@ -36,49 +73,176 @@ class CategoriesViewDataInteractor @Inject constructor(
3673
)
3774
}
3875

39-
private suspend fun getRecordTypeTagViewData(): CategoriesViewData {
40-
val categories = categoryInteractor.getAll()
41-
val isDarkTheme = prefsInteractor.getDarkMode()
76+
private suspend fun getCategoriesViewData(
77+
selectedTypeIds: List<Long>,
78+
searchText: String,
79+
isSearching: Boolean,
80+
): CategoriesViewData = withContext(Dispatchers.Default) {
4281
val result: MutableList<ViewHolderType> = mutableListOf()
4382

44-
categoryViewDataMapper.mapToCategoryHint().let(result::add)
83+
val isDarkTheme = prefsInteractor.getDarkMode()
84+
val categories = categoryInteractor.getAll()
85+
val filteredCategories = filterCategories(
86+
selectedTypeIds = selectedTypeIds,
87+
categories = categories,
88+
).let {
89+
searchCategories(
90+
categories = it,
91+
isSearching = isSearching,
92+
searchText = searchText,
93+
)
94+
}
4595

46-
categories.map { category ->
96+
result += if (filteredCategories.isEmpty() && isSearching) {
97+
mapSearchEmpty()
98+
} else {
99+
categoryViewDataMapper.mapToCategoryHint()
100+
}
101+
102+
result += filteredCategories.map { category ->
47103
categoryViewDataMapper.mapCategory(
48104
category = category,
49105
isDarkTheme = isDarkTheme,
50106
)
51-
}.let(result::addAll)
107+
}
52108

53-
categoryViewDataMapper.mapToTypeTagAddItem(isDarkTheme).let(result::add)
109+
result += categoryViewDataMapper.mapToTypeTagAddItem(isDarkTheme)
54110

55-
return CategoriesViewData(
111+
return@withContext CategoriesViewData(
56112
items = result,
57113
showHint = categories.isNotEmpty(),
58114
)
59115
}
60116

61-
private suspend fun getRecordTagViewData(): CategoriesViewData {
62-
val tags = recordTagInteractor.getAll().filterNot { it.archived }
63-
val types = recordTypeInteractor.getAll().associateBy { it.id }
64-
val isDarkTheme = prefsInteractor.getDarkMode()
117+
private suspend fun getRecordTagViewData(
118+
selectedTypeIds: List<Long>,
119+
searchText: String,
120+
isSearching: Boolean,
121+
): CategoriesViewData = withContext(Dispatchers.Default) {
65122
val result: MutableList<ViewHolderType> = mutableListOf()
66123

67-
categoryViewDataMapper.mapToRecordTagHint().let(result::add)
124+
val isDarkTheme = prefsInteractor.getDarkMode()
125+
val tags = recordTagInteractor.getAll().filterNot { it.archived }
126+
val types = recordTypeInteractor.getAll()
127+
val typesMap = types.associateBy(RecordType::id)
128+
val filteredTags = filterTags(
129+
selectedTypeIds = selectedTypeIds,
130+
tags = tags,
131+
types = types,
132+
).let {
133+
searchTags(
134+
tags = it,
135+
isSearching = isSearching,
136+
searchText = searchText,
137+
)
138+
}
139+
140+
result += if (filteredTags.isEmpty() && isSearching) {
141+
mapSearchEmpty()
142+
} else {
143+
categoryViewDataMapper.mapToRecordTagHint()
144+
}
68145

69-
tags.map { tag ->
146+
result += filteredTags.map { tag ->
70147
categoryViewDataMapper.mapRecordTag(
71148
tag = tag,
72-
type = types[tag.iconColorSource],
149+
type = typesMap[tag.iconColorSource],
73150
isDarkTheme = isDarkTheme,
74151
)
75-
}.let(result::addAll)
152+
}
76153

77-
categoryViewDataMapper.mapToRecordTagAddItem(isDarkTheme).let(result::add)
154+
result += categoryViewDataMapper.mapToRecordTagAddItem(isDarkTheme)
78155

79-
return CategoriesViewData(
156+
return@withContext CategoriesViewData(
80157
items = result,
81158
showHint = tags.isNotEmpty(),
82159
)
83160
}
161+
162+
private fun getBottomEmptySpace(
163+
navBarHeightDp: Int,
164+
): ViewHolderType {
165+
val optionsButtonHeight = resourceRepo.getDimenInDp(R.dimen.button_height)
166+
val size = optionsButtonHeight + navBarHeightDp
167+
return EmptySpaceViewData(
168+
id = "categories_bottom_space".hashCode().toLong(),
169+
height = EmptySpaceViewData.ViewDimension.ExactSizeDp(size),
170+
wrapBefore = true,
171+
)
172+
}
173+
174+
private fun mapSearchEmpty(): ViewHolderType {
175+
return HintViewData(text = resourceRepo.getString(R.string.widget_load_error))
176+
}
177+
178+
private suspend fun filterCategories(
179+
selectedTypeIds: List<Long>,
180+
categories: List<Category>,
181+
): List<Category> {
182+
if (selectedTypeIds.isEmpty()) return categories
183+
184+
val types = recordTypeInteractor.getAll()
185+
val archivedTypeIds = types.filter { it.hidden }.map { it.id }
186+
val recordTypeCategories = recordTypeCategoryInteractor.getAll()
187+
val categoriesToTypeIds = recordTypeCategories
188+
.groupBy { it.categoryId }
189+
.mapValues { (_, value) -> value.map { it.recordTypeId } }
190+
191+
return categories.filter { category ->
192+
val assignedTypes = categoriesToTypeIds[category.id].orEmpty()
193+
// Archived is hidden.
194+
assignedTypes.any { it !in archivedTypeIds && it in selectedTypeIds }
195+
}
196+
}
197+
198+
private suspend fun filterTags(
199+
selectedTypeIds: List<Long>,
200+
tags: List<RecordTag>,
201+
types: List<RecordType>,
202+
): List<RecordTag> {
203+
if (selectedTypeIds.isEmpty()) return tags
204+
205+
val archivedTypeIds = types.filter { it.hidden }.map { it.id }
206+
val tagsToTypes = recordTypeToTagInteractor.getAll()
207+
.groupBy { it.tagId }
208+
.mapValues { (_, value) -> value.map { it.recordTypeId } }
209+
val tagsToDefaultTypes = recordTypeToDefaultTagInteractor.getAll()
210+
.groupBy { it.tagId }
211+
.mapValues { (_, value) -> value.map { it.recordTypeId } }
212+
213+
return tags.filter { tag ->
214+
val hasIconColorSource = tag.iconColorSource in selectedTypeIds
215+
val hasAssignedType = tagsToTypes[tag.id].orEmpty()
216+
.any { it in selectedTypeIds }
217+
// Archived is hidden.
218+
val hasAssignedDefaultType = tagsToDefaultTypes[tag.id].orEmpty()
219+
.any { it !in archivedTypeIds && it in selectedTypeIds }
220+
221+
hasIconColorSource || hasAssignedType || hasAssignedDefaultType
222+
}
223+
}
224+
225+
private fun searchCategories(
226+
categories: List<Category>,
227+
isSearching: Boolean,
228+
searchText: String,
229+
): List<Category> {
230+
return if (isSearching) {
231+
categories.filter { it.name.lowercase().contains(searchText) }
232+
} else {
233+
categories
234+
}
235+
}
236+
237+
private fun searchTags(
238+
tags: List<RecordTag>,
239+
isSearching: Boolean,
240+
searchText: String,
241+
): List<RecordTag> {
242+
return if (isSearching) {
243+
tags.filter { it.name.lowercase().contains(searchText) }
244+
} else {
245+
tags
246+
}
247+
}
84248
}

0 commit comments

Comments
 (0)