From b6418980294d505af432fb0610c0e8d586ea1540 Mon Sep 17 00:00:00 2001 From: razeeman Date: Sun, 12 Jan 2025 21:50:13 +0300 Subject: [PATCH] add duplicate filter to records filter --- .../core/extension/ViewDataExensions.kt | 16 +++ .../core/interactor/RecordFilterInteractor.kt | 27 +++- .../domain/extension/CollectionExtensions.kt | 4 + .../extension/RecordsFilterExtensions.kt | 21 ++- .../GetDuplicatedRecordsInteractor.kt | 76 +++++++++++ .../domain/record/model/RecordsFilter.kt | 7 + .../viewModel/DataEditViewModel.kt | 11 +- .../adapter/RecordsFilterButtonViewData.kt | 1 + .../RecordsFilterUpdateInteractor.kt | 95 +++++++++++-- .../RecordsFilterViewDataInteractor.kt | 127 +++++++++++++----- .../mapper/RecordsFilterViewDataMapper.kt | 90 +++++++++---- .../model/RecordFilterDuplicationsType.kt | 8 ++ .../model/RecordFilterType.kt | 1 + .../RecordsFilterSelectedRecordsViewData.kt | 13 +- .../view/RecordsFilterFragment.kt | 2 +- .../viewModel/RecordsFilterViewModel.kt | 66 ++++++++- ...StatisticsDetailFilterViewModelDelegate.kt | 11 +- .../params/screen/RecordsFilterParam.kt | 11 ++ .../params/screen/RecordsFilterParams.kt | 25 ++-- resources/src/main/res/values-ar/strings.xml | 4 + resources/src/main/res/values-ca/strings.xml | 4 + resources/src/main/res/values-de/strings.xml | 4 + resources/src/main/res/values-es/strings.xml | 4 + resources/src/main/res/values-fa/strings.xml | 4 + resources/src/main/res/values-fr/strings.xml | 4 + resources/src/main/res/values-hi/strings.xml | 4 + resources/src/main/res/values-in/strings.xml | 4 + resources/src/main/res/values-it/strings.xml | 4 + resources/src/main/res/values-iw/strings.xml | 4 + resources/src/main/res/values-ja/strings.xml | 4 + resources/src/main/res/values-ko/strings.xml | 4 + resources/src/main/res/values-nl/strings.xml | 4 + resources/src/main/res/values-pl/strings.xml | 4 + .../src/main/res/values-pt-rPT/strings.xml | 4 + resources/src/main/res/values-pt/strings.xml | 4 + resources/src/main/res/values-ro/strings.xml | 4 + resources/src/main/res/values-ru/strings.xml | 4 + resources/src/main/res/values-sv/strings.xml | 4 + resources/src/main/res/values-tr/strings.xml | 4 + resources/src/main/res/values-uk/strings.xml | 4 + resources/src/main/res/values-vi/strings.xml | 4 + .../src/main/res/values-zh-rTW/strings.xml | 4 + resources/src/main/res/values-zh/strings.xml | 4 + resources/src/main/res/values/strings.xml | 4 + 44 files changed, 598 insertions(+), 114 deletions(-) create mode 100644 domain/src/main/java/com/example/util/simpletimetracker/domain/record/interactor/GetDuplicatedRecordsInteractor.kt create mode 100644 features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordFilterDuplicationsType.kt diff --git a/core/src/main/java/com/example/util/simpletimetracker/core/extension/ViewDataExensions.kt b/core/src/main/java/com/example/util/simpletimetracker/core/extension/ViewDataExensions.kt index 8fd0bef54..ae976aa42 100644 --- a/core/src/main/java/com/example/util/simpletimetracker/core/extension/ViewDataExensions.kt +++ b/core/src/main/java/com/example/util/simpletimetracker/core/extension/ViewDataExensions.kt @@ -125,6 +125,7 @@ fun RecordsFilterParam.toModel(): RecordsFilter { is RecordsFilterParam.DaysOfWeek -> RecordsFilter.DaysOfWeek(items) is RecordsFilterParam.TimeOfDay -> RecordsFilter.TimeOfDay(range.toModel()) is RecordsFilterParam.Duration -> RecordsFilter.Duration(range.toModel()) + is RecordsFilterParam.Duplications -> RecordsFilter.Duplications(items.map { it.toModel() }) } } @@ -142,6 +143,7 @@ fun RecordsFilter.toParams(): RecordsFilterParam { is RecordsFilter.DaysOfWeek -> RecordsFilterParam.DaysOfWeek(items) is RecordsFilter.TimeOfDay -> RecordsFilterParam.TimeOfDay(range.toParams()) is RecordsFilter.Duration -> RecordsFilterParam.Duration(range.toParams()) + is RecordsFilter.Duplications -> RecordsFilterParam.Duplications(items.map { it.toParams() }) } } @@ -189,6 +191,20 @@ fun RecordsFilter.TagItem.toParams(): RecordsFilterParam.TagItem { } } +fun RecordsFilterParam.DuplicationsItem.toModel(): RecordsFilter.DuplicationsItem { + return when (this) { + is RecordsFilterParam.DuplicationsItem.SameActivity -> RecordsFilter.DuplicationsItem.SameActivity + is RecordsFilterParam.DuplicationsItem.SameTimes -> RecordsFilter.DuplicationsItem.SameTimes + } +} + +fun RecordsFilter.DuplicationsItem.toParams(): RecordsFilterParam.DuplicationsItem { + return when (this) { + is RecordsFilter.DuplicationsItem.SameActivity -> RecordsFilterParam.DuplicationsItem.SameActivity + is RecordsFilter.DuplicationsItem.SameTimes -> RecordsFilterParam.DuplicationsItem.SameTimes + } +} + fun RangeLengthParams.toModel(): RangeLength { return when (this) { is RangeLengthParams.Day -> RangeLength.Day diff --git a/core/src/main/java/com/example/util/simpletimetracker/core/interactor/RecordFilterInteractor.kt b/core/src/main/java/com/example/util/simpletimetracker/core/interactor/RecordFilterInteractor.kt index 916508a68..f119556d9 100644 --- a/core/src/main/java/com/example/util/simpletimetracker/core/interactor/RecordFilterInteractor.kt +++ b/core/src/main/java/com/example/util/simpletimetracker/core/interactor/RecordFilterInteractor.kt @@ -29,6 +29,9 @@ import com.example.util.simpletimetracker.domain.recordType.interactor.RecordTyp import com.example.util.simpletimetracker.domain.record.interactor.RunningRecordInteractor import com.example.util.simpletimetracker.domain.record.mapper.RangeMapper import com.example.util.simpletimetracker.domain.daysOfWeek.model.DayOfWeek +import com.example.util.simpletimetracker.domain.record.extension.getDuplicationItems +import com.example.util.simpletimetracker.domain.record.extension.hasDuplicationsFilter +import com.example.util.simpletimetracker.domain.record.interactor.GetDuplicatedRecordsInteractor import com.example.util.simpletimetracker.domain.record.interactor.GetUntrackedRecordsInteractor import com.example.util.simpletimetracker.domain.record.model.Range import com.example.util.simpletimetracker.domain.statistics.model.RangeLength @@ -49,6 +52,7 @@ class RecordFilterInteractor @Inject constructor( private val runningRecordInteractor: RunningRecordInteractor, private val getUntrackedRecordsInteractor: GetUntrackedRecordsInteractor, private val getMultitaskRecordsInteractor: GetMultitaskRecordsInteractor, + private val getDuplicatedRecordsInteractor: GetDuplicatedRecordsInteractor, private val timeMapper: TimeMapper, private val rangeMapper: RangeMapper, private val prefsInteractor: PrefsInteractor, @@ -104,10 +108,11 @@ class RecordFilterInteractor @Inject constructor( val filteredTagItems: List = filters.getFilteredTags() val filteredTaggedIds: List = filteredTagItems.getTaggedIds() val filteredUntagged: Boolean = filteredTagItems.hasUntaggedItem() - val manuallyFilteredIds: List = filters.getManuallyFilteredRecordIds() + val manuallyFilteredIds: Map = filters.getManuallyFilteredRecordIds() val daysOfWeek: List = filters.getDaysOfWeek() val timeOfDay: Range? = filters.getTimeOfDay() val durations: List = filters.getDuration()?.let(::listOf).orEmpty() + val duplicationItems: List = filters.getDuplicationItems() // TODO Use different queries for optimization. // TODO by tag (tagged, untagged). @@ -166,6 +171,17 @@ class RecordFilterInteractor @Inject constructor( } } + val duplicationIds: Map = if (filters.hasDuplicationsFilter()) { + getDuplicatedRecordsInteractor.execute( + filters = duplicationItems, + records = records, + ).let { + it.original + it.duplications + }.associateWith { true } + } else { + emptyMap() + } + // TODO multitask filters. fun RecordBase.selectedByActivity(): Boolean { @@ -211,6 +227,12 @@ class RecordFilterInteractor @Inject constructor( return id in manuallyFilteredIds } + fun RecordBase.selectedByDuplications(): Boolean { + if (duplicationItems.isEmpty()) return true + if (this !is Record) return true + return id in duplicationIds + } + fun RecordBase.selectedByDayOfWeek(): Boolean { if (daysOfWeek.isEmpty()) return true @@ -292,7 +314,8 @@ class RecordFilterInteractor @Inject constructor( !record.isManuallyFiltered() && record.selectedByDayOfWeek() && record.selectedByTimeOfDay() && - record.selectedByDuration() + record.selectedByDuration() && + record.selectedByDuplications() } } diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/extension/CollectionExtensions.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/extension/CollectionExtensions.kt index 19328508e..306333364 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/extension/CollectionExtensions.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/extension/CollectionExtensions.kt @@ -11,6 +11,10 @@ fun MutableSet.addOrRemove(item: T) { if (item in this) remove(item) else add(item) } +fun MutableMap.addOrRemove(item: T, value: U) { + if (item in this) remove(item) else put(item, value) +} + operator fun MutableCollection.plusAssign(element: T?) { if (element != null) this.add(element) } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/record/extension/RecordsFilterExtensions.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/record/extension/RecordsFilterExtensions.kt index b785ea537..15eda6b9b 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/record/extension/RecordsFilterExtensions.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/record/extension/RecordsFilterExtensions.kt @@ -82,10 +82,11 @@ fun List.getFilteredTags(): List { .flatten() } -fun List.getManuallyFilteredRecordIds(): List { +fun List.getManuallyFilteredRecordIds(): Map { return filterIsInstance() .map(RecordsFilter.ManuallyFiltered::recordIds) .flatten() + .associateWith { true } } fun List.getDaysOfWeek(): List { @@ -159,3 +160,21 @@ fun List.hasUncategorizedItem(): Boolean { fun List.hasManuallyFiltered(): Boolean { return any { it is RecordsFilter.ManuallyFiltered } } + +fun List.hasDuplicationsFilter(): Boolean { + return any { it is RecordsFilter.Duplications } +} + +fun List.getDuplicationItems(): List { + return filterIsInstance() + .map(RecordsFilter.Duplications::items) + .flatten() +} + +fun List.hasSameActivity(): Boolean { + return any { it is RecordsFilter.DuplicationsItem.SameActivity } +} + +fun List.hasSameTimes(): Boolean { + return any { it is RecordsFilter.DuplicationsItem.SameTimes } +} diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/record/interactor/GetDuplicatedRecordsInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/record/interactor/GetDuplicatedRecordsInteractor.kt new file mode 100644 index 000000000..cf0cc5447 --- /dev/null +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/record/interactor/GetDuplicatedRecordsInteractor.kt @@ -0,0 +1,76 @@ +package com.example.util.simpletimetracker.domain.record.interactor + +import com.example.util.simpletimetracker.domain.record.extension.hasSameActivity +import com.example.util.simpletimetracker.domain.record.model.Record +import com.example.util.simpletimetracker.domain.record.model.RecordBase +import com.example.util.simpletimetracker.domain.record.model.RecordsFilter +import javax.inject.Inject + +class GetDuplicatedRecordsInteractor @Inject constructor() { + + fun execute( + filters: List, + records: List, + ): Result { + if (filters.isEmpty()) { + return Result( + original = emptyList(), + duplications = emptyList(), + ) + } + val hasSameActivity = filters.hasSameActivity() + + data class Id( + val typeId: Long, + val timeStarted: Long, + val timeEnded: Long, + ) + + val data = mutableMapOf>() + val result = mutableListOf() + val resultDuplications = mutableListOf() + + // Check duplications by adding to map with data class as key. + records.forEach { record -> + if (record !is Record) return@forEach + val id = Id( + typeId = if (hasSameActivity) { + record.typeIds.firstOrNull() ?: return@forEach + } else { + 0L + }, + // Times are always checked and should be in the list. + timeStarted = record.timeStarted, + timeEnded = record.timeEnded, + ) + data[id] = data.getOrElse(key = id, defaultValue = { mutableListOf() }) + .apply { add(record) } + } + + data.forEach { (_, duplications) -> + if (duplications.size < 2) return@forEach + // This record will not be counted as duplication. + val originalRecord = duplications.firstOrNull { + it.tagIds.isNotEmpty() || + it.comment.isNotEmpty() + } ?: duplications.firstOrNull() + duplications.forEach { record -> + if (record.id == originalRecord?.id) { + result += record.id + } else { + resultDuplications += record.id + } + } + } + + return Result( + original = result, + duplications = resultDuplications, + ) + } + + data class Result( + val original: List, + val duplications: List, + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/record/model/RecordsFilter.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/record/model/RecordsFilter.kt index 70d00f7e4..237c38b34 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/record/model/RecordsFilter.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/record/model/RecordsFilter.kt @@ -30,6 +30,8 @@ sealed interface RecordsFilter { data class Duration(val range: Range) : RecordsFilter // duration-from, duration-to in range. + data class Duplications(val items: List) : RecordsFilter + sealed interface CommentItem { object NoComment : CommentItem object AnyComment : CommentItem @@ -45,4 +47,9 @@ sealed interface RecordsFilter { data class Tagged(val tagId: Long) : TagItem object Untagged : TagItem } + + sealed interface DuplicationsItem { + object SameActivity : DuplicationsItem + object SameTimes : DuplicationsItem + } } \ No newline at end of file diff --git a/features/feature_data_edit/src/main/java/com/example/util/simpletimetracker/feature_data_edit/viewModel/DataEditViewModel.kt b/features/feature_data_edit/src/main/java/com/example/util/simpletimetracker/feature_data_edit/viewModel/DataEditViewModel.kt index dbb87abae..ef6430f21 100644 --- a/features/feature_data_edit/src/main/java/com/example/util/simpletimetracker/feature_data_edit/viewModel/DataEditViewModel.kt +++ b/features/feature_data_edit/src/main/java/com/example/util/simpletimetracker/feature_data_edit/viewModel/DataEditViewModel.kt @@ -95,10 +95,13 @@ class DataEditViewModel @Inject constructor( RecordsFilterParams( tag = FILTER_TAG, title = resourceRepo.getString(R.string.chart_filter_hint), - dateSelectionAvailable = true, - untrackedSelectionAvailable = false, - multitaskSelectionAvailable = false, - addRunningRecords = false, + flags = RecordsFilterParams.Flags( + dateSelectionAvailable = true, + untrackedSelectionAvailable = false, + multitaskSelectionAvailable = false, + duplicationsSelectionAvailable = true, + addRunningRecords = false, + ), filters = filters.map(RecordsFilter::toParams), defaultLastDaysNumber = 7, ).let(router::navigate) diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/adapter/RecordsFilterButtonViewData.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/adapter/RecordsFilterButtonViewData.kt index 5604a9665..372072808 100644 --- a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/adapter/RecordsFilterButtonViewData.kt +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/adapter/RecordsFilterButtonViewData.kt @@ -14,5 +14,6 @@ data class RecordsFilterButtonViewData( enum class Type { INVERT_SELECTION, + FILTER_DUPLICATES, } } \ No newline at end of file diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterUpdateInteractor.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterUpdateInteractor.kt index 2c4311748..155c2ad18 100644 --- a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterUpdateInteractor.kt +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterUpdateInteractor.kt @@ -1,12 +1,16 @@ package com.example.util.simpletimetracker.feature_records_filter.interactor +import com.example.util.simpletimetracker.core.interactor.RecordFilterInteractor import com.example.util.simpletimetracker.domain.base.UNCATEGORIZED_ITEM_ID import com.example.util.simpletimetracker.domain.category.model.Category +import com.example.util.simpletimetracker.domain.category.model.RecordTypeCategory +import com.example.util.simpletimetracker.domain.daysOfWeek.model.DayOfWeek import com.example.util.simpletimetracker.domain.extension.addOrRemove import com.example.util.simpletimetracker.domain.record.extension.getAllTypeIds import com.example.util.simpletimetracker.domain.record.extension.getCategoryItems import com.example.util.simpletimetracker.domain.record.extension.getCommentItems import com.example.util.simpletimetracker.domain.record.extension.getDaysOfWeek +import com.example.util.simpletimetracker.domain.record.extension.getDuplicationItems import com.example.util.simpletimetracker.domain.record.extension.getFilteredTags import com.example.util.simpletimetracker.domain.record.extension.getManuallyFilteredRecordIds import com.example.util.simpletimetracker.domain.record.extension.getSelectedTags @@ -14,21 +18,21 @@ import com.example.util.simpletimetracker.domain.record.extension.getTypeIds import com.example.util.simpletimetracker.domain.record.extension.getTypeIdsFromCategories import com.example.util.simpletimetracker.domain.record.extension.hasMultitaskFilter import com.example.util.simpletimetracker.domain.record.extension.hasUntrackedFilter -import com.example.util.simpletimetracker.domain.recordTag.interactor.FilterSelectableTagsInteractor -import com.example.util.simpletimetracker.domain.daysOfWeek.model.DayOfWeek +import com.example.util.simpletimetracker.domain.record.interactor.GetDuplicatedRecordsInteractor import com.example.util.simpletimetracker.domain.record.model.Range -import com.example.util.simpletimetracker.domain.statistics.model.RangeLength +import com.example.util.simpletimetracker.domain.record.model.RecordsFilter +import com.example.util.simpletimetracker.domain.recordTag.interactor.FilterSelectableTagsInteractor import com.example.util.simpletimetracker.domain.recordTag.model.RecordTag -import com.example.util.simpletimetracker.domain.recordType.model.RecordType -import com.example.util.simpletimetracker.domain.category.model.RecordTypeCategory import com.example.util.simpletimetracker.domain.recordTag.model.RecordTypeToTag -import com.example.util.simpletimetracker.domain.record.model.RecordsFilter +import com.example.util.simpletimetracker.domain.recordType.model.RecordType +import com.example.util.simpletimetracker.domain.statistics.model.RangeLength import com.example.util.simpletimetracker.feature_base_adapter.category.CategoryViewData import com.example.util.simpletimetracker.feature_base_adapter.record.RecordViewData import com.example.util.simpletimetracker.feature_base_adapter.recordFilter.FilterViewData import com.example.util.simpletimetracker.feature_records_filter.mapper.RecordsFilterViewDataMapper import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterCommentType import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterDateType +import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterDuplicationsType import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterType import com.example.util.simpletimetracker.feature_records_filter.model.RecordsFilterSelectedRecordsViewData import com.example.util.simpletimetracker.feature_records_filter.viewData.RecordsFilterSelectionButtonType @@ -37,6 +41,8 @@ import javax.inject.Inject class RecordsFilterUpdateInteractor @Inject constructor( private val filterSelectableTagsInteractor: FilterSelectableTagsInteractor, private val recordsFilterViewDataMapper: RecordsFilterViewDataMapper, + private val getDuplicatedRecordsInteractor: GetDuplicatedRecordsInteractor, + private val recordFilterInteractor: RecordFilterInteractor, ) { fun handleTypeClick( @@ -148,6 +154,47 @@ class RecordsFilterUpdateInteractor @Inject constructor( return filters } + fun handleDuplicationsFilterClick( + currentFilters: List, + itemType: FilterViewData.Type, + ): List { + val filters = currentFilters.toMutableList() + val currentItems = filters.getDuplicationItems() + + val clickedItem = when (itemType) { + is RecordFilterDuplicationsType.SameActivity -> { + RecordsFilter.DuplicationsItem.SameActivity + } + is RecordFilterDuplicationsType.SameTimes -> { + RecordsFilter.DuplicationsItem.SameTimes + } + else -> return currentFilters + } + val hasDefaultItem = currentItems.any { it is RecordsFilter.DuplicationsItem.SameTimes } + val defaultItemIsClicked = clickedItem is RecordsFilter.DuplicationsItem.SameTimes + val newItems = currentItems.toMutableList().apply { + when { + hasDefaultItem && defaultItemIsClicked -> { + // Remove all filters if default will be removed. + clear() + } + !hasDefaultItem && !defaultItemIsClicked -> { + // Add default filter if it is not added. + add(RecordsFilter.DuplicationsItem.SameTimes) + addOrRemove(clickedItem) + } + else -> { + addOrRemove(clickedItem) + } + } + } + + filters.removeAll { it is RecordsFilter.Duplications } + if (newItems.isNotEmpty()) filters.add(RecordsFilter.Duplications(newItems)) + + return filters + } + fun handleCommentFilterClick( currentFilters: List, itemType: FilterViewData.Type, @@ -194,10 +241,10 @@ class RecordsFilterUpdateInteractor @Inject constructor( ): List { val filters = currentFilters.toMutableList() val newIds = filters.getManuallyFilteredRecordIds() - .toMutableList() - .apply { addOrRemove(id) } + .toMutableMap() + .apply { addOrRemove(id, true) } filters.removeAll { it is RecordsFilter.ManuallyFiltered } - if (newIds.isNotEmpty()) filters.add(RecordsFilter.ManuallyFiltered(newIds)) + if (newIds.isNotEmpty()) filters.add(RecordsFilter.ManuallyFiltered(newIds.keys.toList())) return filters } @@ -205,12 +252,12 @@ class RecordsFilterUpdateInteractor @Inject constructor( currentFilters: List, recordsViewData: RecordsFilterSelectedRecordsViewData?, ): List { + if (recordsViewData == null || recordsViewData.isLoading) return currentFilters + val filters = currentFilters.toMutableList() val filteredIds = filters.getManuallyFilteredRecordIds() - .toMutableList() val selectedIds = recordsViewData - ?.recordsViewData - .orEmpty() + .recordsViewData .filterIsInstance() .filter { it.id !in filteredIds } .map { it.id } @@ -220,6 +267,30 @@ class RecordsFilterUpdateInteractor @Inject constructor( return filters } + suspend fun handleFilterDuplicates( + currentFilters: List, + recordsViewData: RecordsFilterSelectedRecordsViewData?, + ): List { + if (recordsViewData == null || recordsViewData.isLoading) return currentFilters + + val filters = currentFilters.toMutableList() + filters.removeAll { it is RecordsFilter.ManuallyFiltered } + val records = recordFilterInteractor.getByFilter(filters) + val result = getDuplicatedRecordsInteractor.execute( + filters = filters.getDuplicationItems(), + records = records, + ) + val selectedIds = recordsViewData + .recordsViewData + .mapNotNull { + if (it !is RecordViewData.Tracked) return@mapNotNull null + if (it.id in result.duplications) it.id else null + } + + if (selectedIds.isNotEmpty()) filters.add(RecordsFilter.ManuallyFiltered(selectedIds)) + return filters + } + fun onDurationSet( currentFilters: List, rangeStart: Long, diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterViewDataInteractor.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterViewDataInteractor.kt index d2304905e..cf12b9f54 100644 --- a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterViewDataInteractor.kt +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterViewDataInteractor.kt @@ -44,6 +44,7 @@ import com.example.util.simpletimetracker.domain.record.model.Record import com.example.util.simpletimetracker.domain.recordTag.model.RecordTag import com.example.util.simpletimetracker.domain.recordType.model.RecordType import com.example.util.simpletimetracker.domain.category.model.RecordTypeCategory +import com.example.util.simpletimetracker.domain.record.extension.hasDuplicationsFilter import com.example.util.simpletimetracker.domain.recordType.model.RecordTypeGoal import com.example.util.simpletimetracker.domain.recordTag.model.RecordTypeToTag import com.example.util.simpletimetracker.domain.record.model.RecordsFilter @@ -60,6 +61,7 @@ import com.example.util.simpletimetracker.feature_records_filter.adapter.Records import com.example.util.simpletimetracker.feature_records_filter.adapter.RecordsFilterRangeViewData import com.example.util.simpletimetracker.feature_records_filter.mapper.RecordsFilterViewDataMapper import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterCommentType +import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterDuplicationsType import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterType import com.example.util.simpletimetracker.feature_records_filter.model.RecordsFilterSelectedRecordsViewData import com.example.util.simpletimetracker.feature_records_filter.model.RecordsFilterSelectionState @@ -147,47 +149,58 @@ class RecordsFilterViewDataInteractor @Inject constructor( val useMilitaryTime = prefsInteractor.getUseMilitaryTimeFormat() val useProportionalMinutes = prefsInteractor.getUseProportionalMinutes() val showSeconds = prefsInteractor.getShowSeconds() + val manuallyFilteredIds = filters.getManuallyFilteredRecordIds() + .keys.toList() val finalFilters = filters .takeUnless { // If date isn't available and no other filters - // show empty records even if date is present. - !extra.dateSelectionAvailable && filters.none { it !is RecordsFilter.Date } + !extra.flags.dateSelectionAvailable && + filters.none { it !is RecordsFilter.Date } } .orEmpty() - val records = recordFilterInteractor.getByFilter(finalFilters) - .let { if (extra.addRunningRecords) it else it.filterIsInstance() } - val manuallyFilteredRecords = filters - .getManuallyFilteredRecordIds() - .mapNotNull { recordInteractor.get(it) } // TODO do better - .mapNotNull { record -> - val mapped = recordViewDataMapper.mapFilteredRecord( - record = record, - recordTypes = recordTypes, - allRecordTags = recordTags, - isDarkTheme = isDarkTheme, - useMilitaryTime = useMilitaryTime, - useProportionalMinutes = useProportionalMinutes, - showSeconds = showSeconds, - isFiltered = true, - ) ?: return@mapNotNull null - record.timeStarted to mapped + .toMutableList() + .apply { + // Filtered records are marked separately here, so need records with them. + // But of only manual filter selected - leave filter, otherwise records would be empty. + if (filters.any { it !is RecordsFilter.ManuallyFiltered }) { + removeAll { it is RecordsFilter.ManuallyFiltered } + } } + val records = recordFilterInteractor.getByFilter(finalFilters) + .let { if (extra.flags.addRunningRecords) it else it.filterIsInstance() } + var count: Int + var filtered = 0 val viewData = records .mapNotNull { record -> ensureActive() val viewData = when (record) { is Record -> if (record.typeId != UNTRACKED_ITEM_ID) { - recordViewDataMapper.map( - record = record, - recordType = recordTypes[record.typeId] ?: return@mapNotNull null, - recordTags = recordTags.filter { it.id in record.tagIds }, - isDarkTheme = isDarkTheme, - useMilitaryTime = useMilitaryTime, - useProportionalMinutes = useProportionalMinutes, - showSeconds = showSeconds, - ) + if (record.id in manuallyFilteredIds) { + filtered += 1 + recordViewDataMapper.mapFilteredRecord( + record = record, + recordTypes = recordTypes, + allRecordTags = recordTags, + isDarkTheme = isDarkTheme, + useMilitaryTime = useMilitaryTime, + useProportionalMinutes = useProportionalMinutes, + showSeconds = showSeconds, + isFiltered = true, + ) ?: return@mapNotNull null + } else { + recordViewDataMapper.map( + record = record, + recordType = recordTypes[record.typeId] ?: return@mapNotNull null, + recordTags = recordTags.filter { it.id in record.tagIds }, + isDarkTheme = isDarkTheme, + useMilitaryTime = useMilitaryTime, + useProportionalMinutes = useProportionalMinutes, + showSeconds = showSeconds, + ) + } } else { recordViewDataMapper.mapToUntracked( timeStarted = record.timeStarted, @@ -223,8 +236,7 @@ class RecordsFilterViewDataInteractor @Inject constructor( } record.timeStarted to viewData } - .also { count = it.size } - .plus(manuallyFilteredRecords) + .also { count = it.size - filtered } .sortedByDescending { (timeStarted, _) -> timeStarted } .let(dateDividerViewDataMapper::addDateViewData) .ifEmpty { listOf(recordViewDataMapper.mapToEmpty()) } @@ -264,10 +276,11 @@ class RecordsFilterViewDataInteractor @Inject constructor( RecordFilterType.Comment.takeUnless { hasUntracked || hasMultitask }, RecordFilterType.SelectedTags.takeUnless { hasUntracked || hasMultitask }, RecordFilterType.FilteredTags.takeUnless { hasUntracked || hasMultitask }, - RecordFilterType.Date.takeIf { extra.dateSelectionAvailable }, + RecordFilterType.Date.takeIf { extra.flags.dateSelectionAvailable }, RecordFilterType.DaysOfWeek, RecordFilterType.TimeOfDay, RecordFilterType.Duration, + RecordFilterType.Duplications.takeIf { extra.flags.duplicationsSelectionAvailable }, RecordFilterType.ManuallyFiltered.takeIf { filters.hasManuallyFiltered() && !hasUntracked && !hasMultitask }, @@ -375,20 +388,20 @@ class RecordsFilterViewDataInteractor @Inject constructor( } if ( - extra.untrackedSelectionAvailable || - extra.multitaskSelectionAvailable + extra.flags.untrackedSelectionAvailable || + extra.flags.multitaskSelectionAvailable ) { DividerViewData(2).let(result::add) } - if (extra.untrackedSelectionAvailable) { + if (extra.flags.untrackedSelectionAvailable) { categoryViewDataMapper.mapToTagUntrackedItem( isFiltered = !filters.hasUntrackedFilter(), isDarkTheme = isDarkTheme, ).let(result::add) } - if (extra.multitaskSelectionAvailable) { + if (extra.flags.multitaskSelectionAvailable) { categoryViewDataMapper.mapToMultitaskItem( isFiltered = !filters.hasMultitaskFilter(), isDarkTheme = isDarkTheme, @@ -434,6 +447,40 @@ class RecordsFilterViewDataInteractor @Inject constructor( return@withContext result } + suspend fun getDuplicationsFilterSelectionViewData( + filters: List, + ): List = withContext(Dispatchers.Default) { + val result: MutableList = mutableListOf() + val isDarkTheme = prefsInteractor.getDarkMode() + val duplicationsFilters = listOf( + RecordFilterDuplicationsType.SameActivity, + RecordFilterDuplicationsType.SameTimes, + ) + val button = RecordsFilterButtonViewData( + type = RecordsFilterButtonViewData.Type.FILTER_DUPLICATES, + text = resourceRepo.getString(R.string.records_filter_duplications_manually_fitler), + ) + + result += EmptySpaceViewData( + id = 1, + width = EmptySpaceViewData.ViewDimension.MatchParent, + height = EmptySpaceViewData.ViewDimension.ExactSizeDp(6), + ) + result += duplicationsFilters.map { + mapper.mapDuplicationsFilter( + type = it, + filters = filters, + isDarkTheme = isDarkTheme, + ) + } + if (filters.hasDuplicationsFilter()) { + result += DividerViewData(1) + result += button + } + + return@withContext result + } + suspend fun getTagsFilterSelectionViewData( type: RecordFilterType, filters: List, @@ -563,6 +610,7 @@ class RecordsFilterViewDataInteractor @Inject constructor( return@withContext result } + // TODO add loader when a lot of records filtered. suspend fun getManualFilterSelectionViewData( filters: List, recordTypes: Map, @@ -572,14 +620,19 @@ class RecordsFilterViewDataInteractor @Inject constructor( val useMilitaryTime = prefsInteractor.getUseMilitaryTimeFormat() val useProportionalMinutes = prefsInteractor.getUseProportionalMinutes() val showSeconds = prefsInteractor.getShowSeconds() + val manuallyFilteredIds = filters.getManuallyFilteredRecordIds() + .keys.toList() val button = RecordsFilterButtonViewData( type = RecordsFilterButtonViewData.Type.INVERT_SELECTION, text = resourceRepo.getString(R.string.records_filter_invert_selection), ) + val manuallyFilteredRecords = if (manuallyFilteredIds.size > 10) { + recordInteractor.getAll().filter { it.id in manuallyFilteredIds } + } else { + manuallyFilteredIds.mapNotNull { recordInteractor.get(it) } + } - return@withContext button.let(::listOf) + filters - .getManuallyFilteredRecordIds() - .mapNotNull { recordInteractor.get(it) } // TODO do better + return@withContext button.let(::listOf) + manuallyFilteredRecords .mapNotNull { record -> val mapped = recordViewDataMapper.mapFilteredRecord( record = record, diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/mapper/RecordsFilterViewDataMapper.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/mapper/RecordsFilterViewDataMapper.kt index 75cbd1d7a..cfbc42015 100644 --- a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/mapper/RecordsFilterViewDataMapper.kt +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/mapper/RecordsFilterViewDataMapper.kt @@ -10,6 +10,9 @@ import com.example.util.simpletimetracker.domain.record.extension.getComments import com.example.util.simpletimetracker.domain.record.extension.hasAnyComment import com.example.util.simpletimetracker.domain.record.extension.hasNoComment import com.example.util.simpletimetracker.domain.daysOfWeek.model.DayOfWeek +import com.example.util.simpletimetracker.domain.record.extension.getDuplicationItems +import com.example.util.simpletimetracker.domain.record.extension.hasSameActivity +import com.example.util.simpletimetracker.domain.record.extension.hasSameTimes import com.example.util.simpletimetracker.domain.statistics.model.RangeLength import com.example.util.simpletimetracker.domain.record.model.RecordsFilter import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType @@ -19,6 +22,7 @@ import com.example.util.simpletimetracker.feature_base_adapter.selectionButton.S import com.example.util.simpletimetracker.feature_records_filter.R import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterCommentType import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterDateType +import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterDuplicationsType import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterType import com.example.util.simpletimetracker.feature_records_filter.viewData.RecordsFilterSelectionButtonType import com.example.util.simpletimetracker.navigation.params.screen.RecordsFilterParams @@ -38,13 +42,13 @@ class RecordsFilterViewDataMapper @Inject constructor( return filters .firstOrNull { when (it) { - is RecordsFilter.Date -> extra.dateSelectionAvailable - is RecordsFilter.Untracked -> extra.untrackedSelectionAvailable - is RecordsFilter.Multitask -> extra.multitaskSelectionAvailable + is RecordsFilter.Date -> extra.flags.dateSelectionAvailable + is RecordsFilter.Untracked -> extra.flags.untrackedSelectionAvailable + is RecordsFilter.Multitask -> extra.flags.multitaskSelectionAvailable else -> true } } - ?.let { mapToViewData(it::class.java) } + ?.let(::mapToViewData) } fun mapRecordsCount( @@ -79,6 +83,7 @@ class RecordsFilterViewDataMapper @Inject constructor( RecordFilterType.DaysOfWeek -> R.string.range_day RecordFilterType.TimeOfDay -> R.string.date_time_dialog_time RecordFilterType.Duration -> R.string.records_all_sort_duration + RecordFilterType.Duplications -> R.string.records_filter_duplications }.let(resourceRepo::getString) } @@ -88,16 +93,15 @@ class RecordsFilterViewDataMapper @Inject constructor( startOfDayShift: Long, firstDayOfWeek: DayOfWeek, ): String { - val filterName = filter::class.java + val filterName = filter .let(::mapToViewData) - ?.let(::mapInactiveFilterName) - .orEmpty() + .let(::mapInactiveFilterName) val filterValue = when (filter) { - is RecordsFilter.Untracked -> { - "" - } - is RecordsFilter.Multitask -> { + is RecordsFilter.Untracked, + is RecordsFilter.Multitask, + is RecordsFilter.Duplications, + -> { "" } is RecordsFilter.Activity -> { @@ -204,6 +208,39 @@ class RecordsFilterViewDataMapper @Inject constructor( ) } + fun mapDuplicationsFilter( + type: RecordFilterDuplicationsType, + filters: List, + isDarkTheme: Boolean, + ): ViewHolderType { + val name: String + val enabled: Boolean + + when (type) { + RecordFilterDuplicationsType.SameActivity -> { + enabled = filters.getDuplicationItems().hasSameActivity() + name = resourceRepo.getString(R.string.records_filter_duplications_same_activity) + } + RecordFilterDuplicationsType.SameTimes -> { + enabled = filters.getDuplicationItems().hasSameTimes() + name = resourceRepo.getString(R.string.records_filter_duplications_same_times) + } + } + + return FilterViewData( + id = type.hashCode().toLong(), + type = type, + name = name, + color = if (enabled) { + colorMapper.toActiveColor(isDarkTheme) + } else { + colorMapper.toInactiveColor(isDarkTheme) + }, + selected = enabled, + removeBtnVisible = false, + ) + } + fun mapDateRangeFilter( rangeLength: RangeLength, filter: RecordsFilter.Date?, @@ -249,6 +286,7 @@ class RecordsFilterViewDataMapper @Inject constructor( RecordFilterType.DaysOfWeek -> RecordsFilter.DaysOfWeek::class.java RecordFilterType.TimeOfDay -> RecordsFilter.TimeOfDay::class.java RecordFilterType.Duration -> RecordsFilter.Duration::class.java + RecordFilterType.Duplications -> RecordsFilter.Duplications::class.java } } @@ -296,21 +334,21 @@ class RecordsFilterViewDataMapper @Inject constructor( } } - private fun mapToViewData(clazz: Class): RecordFilterType? { - return when (clazz) { - RecordsFilter.Untracked::class.java -> RecordFilterType.Untracked - RecordsFilter.Multitask::class.java -> RecordFilterType.Multitask - RecordsFilter.Activity::class.java -> RecordFilterType.Activity - RecordsFilter.Category::class.java -> RecordFilterType.Category - RecordsFilter.Comment::class.java -> RecordFilterType.Comment - RecordsFilter.Date::class.java -> RecordFilterType.Date - RecordsFilter.SelectedTags::class.java -> RecordFilterType.SelectedTags - RecordsFilter.FilteredTags::class.java -> RecordFilterType.FilteredTags - RecordsFilter.ManuallyFiltered::class.java -> RecordFilterType.ManuallyFiltered - RecordsFilter.DaysOfWeek::class.java -> RecordFilterType.DaysOfWeek - RecordsFilter.TimeOfDay::class.java -> RecordFilterType.TimeOfDay - RecordsFilter.Duration::class.java -> RecordFilterType.Duration - else -> null + private fun mapToViewData(filter: RecordsFilter): RecordFilterType { + return when (filter) { + is RecordsFilter.Untracked -> RecordFilterType.Untracked + is RecordsFilter.Multitask -> RecordFilterType.Multitask + is RecordsFilter.Activity -> RecordFilterType.Activity + is RecordsFilter.Category -> RecordFilterType.Category + is RecordsFilter.Comment -> RecordFilterType.Comment + is RecordsFilter.Date -> RecordFilterType.Date + is RecordsFilter.SelectedTags -> RecordFilterType.SelectedTags + is RecordsFilter.FilteredTags -> RecordFilterType.FilteredTags + is RecordsFilter.ManuallyFiltered -> RecordFilterType.ManuallyFiltered + is RecordsFilter.DaysOfWeek -> RecordFilterType.DaysOfWeek + is RecordsFilter.TimeOfDay -> RecordFilterType.TimeOfDay + is RecordsFilter.Duration -> RecordFilterType.Duration + is RecordsFilter.Duplications -> RecordFilterType.Duplications } } } \ No newline at end of file diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordFilterDuplicationsType.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordFilterDuplicationsType.kt new file mode 100644 index 000000000..5a7d82b2a --- /dev/null +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordFilterDuplicationsType.kt @@ -0,0 +1,8 @@ +package com.example.util.simpletimetracker.feature_records_filter.model + +import com.example.util.simpletimetracker.feature_base_adapter.recordFilter.FilterViewData + +sealed interface RecordFilterDuplicationsType : FilterViewData.Type { + object SameActivity : RecordFilterDuplicationsType + object SameTimes : RecordFilterDuplicationsType +} \ No newline at end of file diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordFilterType.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordFilterType.kt index e7680512f..dc317b4fe 100644 --- a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordFilterType.kt +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordFilterType.kt @@ -15,4 +15,5 @@ sealed interface RecordFilterType : FilterViewData.Type { object DaysOfWeek : RecordFilterType object TimeOfDay : RecordFilterType object Duration : RecordFilterType + object Duplications : RecordFilterType } \ No newline at end of file diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordsFilterSelectedRecordsViewData.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordsFilterSelectedRecordsViewData.kt index 312ced1cb..6822f4d68 100644 --- a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordsFilterSelectedRecordsViewData.kt +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/model/RecordsFilterSelectedRecordsViewData.kt @@ -1,21 +1,10 @@ package com.example.util.simpletimetracker.feature_records_filter.model import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType -import com.example.util.simpletimetracker.feature_base_adapter.loader.LoaderViewData data class RecordsFilterSelectedRecordsViewData( val isLoading: Boolean, val selectedRecordsCount: String, val showListButtonIsVisible: Boolean, val recordsViewData: List, -) { - - companion object { - val Loading = RecordsFilterSelectedRecordsViewData( - isLoading = true, - selectedRecordsCount = "", - showListButtonIsVisible = false, - recordsViewData = listOf(LoaderViewData()), - ) - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/view/RecordsFilterFragment.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/view/RecordsFilterFragment.kt index 5c1f35baa..d0ef5d374 100644 --- a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/view/RecordsFilterFragment.kt +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/view/RecordsFilterFragment.kt @@ -179,7 +179,7 @@ class RecordsFilterFragment : tvRecordsFilterTitle.isInvisible = viewData.isLoading tvRecordsFilterTitle.text = viewData.selectedRecordsCount ivRecordsFilterShowList.isVisible = !viewData.isLoading && viewData.showListButtonIsVisible - recordsAdapter.replaceAsNew(viewData.recordsViewData) + recordsAdapter.replace(viewData.recordsViewData) } private fun showKeyboard(visible: Boolean) { diff --git a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/viewModel/RecordsFilterViewModel.kt b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/viewModel/RecordsFilterViewModel.kt index 77216f703..e885c83ec 100644 --- a/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/viewModel/RecordsFilterViewModel.kt +++ b/features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/viewModel/RecordsFilterViewModel.kt @@ -46,6 +46,7 @@ import com.example.util.simpletimetracker.feature_records_filter.interactor.Reco import com.example.util.simpletimetracker.feature_records_filter.mapper.RecordsFilterViewDataMapper import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterCommentType import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterDateType +import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterDuplicationsType import com.example.util.simpletimetracker.feature_records_filter.model.RecordFilterType import com.example.util.simpletimetracker.feature_records_filter.model.RecordsFilterSelectedRecordsViewData import com.example.util.simpletimetracker.feature_records_filter.model.RecordsFilterSelectionState @@ -104,7 +105,7 @@ class RecordsFilterViewModel @Inject constructor( val recordsViewData: LiveData by lazy { return@lazy MutableLiveData().let { initial -> viewModelScope.launch { - initial.value = RecordsFilterSelectedRecordsViewData.Loading + initial.value = getRecordsLoadState(showLoader = true) initial.value = loadRecordsViewData() } initial @@ -124,6 +125,7 @@ class RecordsFilterViewModel @Inject constructor( private var filtersLoadJob: Job? = null private var filtersSelectionLoadJob: Job? = null private var recordsLoadJob: Job? = null + private var filterDuplicationsJob: Job? = null // Cache private var types: List = emptyList() @@ -227,6 +229,7 @@ class RecordsFilterViewModel @Inject constructor( when (item.type) { is RecordFilterCommentType -> handleCommentFilterClick(item) is RecordFilterDateType -> onDateRangeClick(item) + is RecordFilterDuplicationsType -> handleDuplicationsFilterClick(item) else -> { // Do nothing. } @@ -297,7 +300,7 @@ class RecordsFilterViewModel @Inject constructor( ) { if (item is RecordViewData.Untracked) return // TODO manually filter untracked records? handleRecordClick(item.getUniqueId()) - updateViewDataOnFiltersChanged() + updateViewDataOnFiltersChanged(showLoader = false) } fun onInnerFilterButtonClick(viewData: RecordsFilterButtonViewData) { @@ -305,8 +308,10 @@ class RecordsFilterViewModel @Inject constructor( RecordsFilterButtonViewData.Type.INVERT_SELECTION -> { handleInvertSelection() } + RecordsFilterButtonViewData.Type.FILTER_DUPLICATES -> { + handleFilterDuplicates() + } } - updateViewDataOnFiltersChanged() } fun onDayOfWeekClick(viewData: DayOfWeekViewData) { @@ -369,6 +374,13 @@ class RecordsFilterViewModel @Inject constructor( ) } + private fun handleDuplicationsFilterClick(item: FilterViewData) { + filters = recordsFilterUpdateInteractor.handleDuplicationsFilterClick( + currentFilters = filters, + itemType = item.type, + ) + } + private fun handleCommentChange(text: String) { filters = recordsFilterUpdateInteractor.handleCommentChange( currentFilters = filters, @@ -423,6 +435,19 @@ class RecordsFilterViewModel @Inject constructor( recordsViewData = recordsViewData.value, ) checkManualFilterVisibility() + updateViewDataOnFiltersChanged() + } + + private fun handleFilterDuplicates() { + filterDuplicationsJob?.cancel() + filterDuplicationsJob = viewModelScope.launch { + filters = recordsFilterUpdateInteractor.handleFilterDuplicates( + currentFilters = filters, + recordsViewData = recordsViewData.value, + ) + checkManualFilterVisibility() + updateViewDataOnFiltersChanged() + } } private fun handleDayOfWeekClick(dayOfWeek: DayOfWeek) { @@ -550,10 +575,12 @@ class RecordsFilterViewModel @Inject constructor( updateViewDataOnFiltersChanged() } - private fun updateViewDataOnFiltersChanged() { + private fun updateViewDataOnFiltersChanged( + showLoader: Boolean = true, + ) { updateFilters() updateFilterSelectionViewData() - updateRecords() + updateRecords(showLoader) } private suspend fun getCurrentRange(): Range { @@ -566,6 +593,23 @@ class RecordsFilterViewModel @Inject constructor( } } + private fun getRecordsLoadState( + showLoader: Boolean, + ): RecordsFilterSelectedRecordsViewData { + val currentState = recordsViewData.value + + return RecordsFilterSelectedRecordsViewData( + isLoading = true, + selectedRecordsCount = "", + showListButtonIsVisible = false, + recordsViewData = if (showLoader) { + listOf(LoaderViewData()) + } else { + currentState?.recordsViewData.orEmpty() + }, + ) + } + private suspend fun getTypesCache(): List { return types.takeUnless { it.isEmpty() } ?: run { recordTypeInteractor.getAll().also { types = it } } @@ -632,10 +676,12 @@ class RecordsFilterViewModel @Inject constructor( ) } - private fun updateRecords() { + private fun updateRecords( + showLoader: Boolean, + ) { recordsLoadJob?.cancel() recordsLoadJob = viewModelScope.launch { - recordsViewData.set(RecordsFilterSelectedRecordsViewData.Loading) + recordsViewData.set(getRecordsLoadState(showLoader)) val data = loadRecordsViewData() recordsViewData.set(data) } @@ -652,6 +698,7 @@ class RecordsFilterViewModel @Inject constructor( } private fun updateFilterSelectionViewData() { + if (filterSelectionState is RecordsFilterSelectionState.Hidden) return filtersSelectionLoadJob?.cancel() filtersSelectionLoadJob = viewModelScope.launch { val data = loadFilterSelectionViewData() @@ -724,6 +771,11 @@ class RecordsFilterViewModel @Inject constructor( defaultRange = defaultDurationRange, ) } + RecordFilterType.Duplications -> { + viewDataInteractor.getDuplicationsFilterSelectionViewData( + filters = filters, + ) + } } } diff --git a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/viewModel/delegate/StatisticsDetailFilterViewModelDelegate.kt b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/viewModel/delegate/StatisticsDetailFilterViewModelDelegate.kt index 06aa21997..291754cf8 100644 --- a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/viewModel/delegate/StatisticsDetailFilterViewModelDelegate.kt +++ b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/viewModel/delegate/StatisticsDetailFilterViewModelDelegate.kt @@ -118,10 +118,13 @@ class StatisticsDetailFilterViewModelDelegate @Inject constructor( RecordsFilterParams( tag = tag, title = title, - dateSelectionAvailable = false, - untrackedSelectionAvailable = true, - multitaskSelectionAvailable = true, - addRunningRecords = true, + flags = RecordsFilterParams.Flags( + dateSelectionAvailable = false, + untrackedSelectionAvailable = true, + multitaskSelectionAvailable = true, + duplicationsSelectionAvailable = false, + addRunningRecords = true, + ), filters = filters .plus(parent.getDateFilter()) .map(RecordsFilter::toParams).toList(), diff --git a/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/RecordsFilterParam.kt b/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/RecordsFilterParam.kt index 31bda2f14..475ce8904 100644 --- a/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/RecordsFilterParam.kt +++ b/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/RecordsFilterParam.kt @@ -42,6 +42,9 @@ sealed interface RecordsFilterParam : Parcelable { @Parcelize data class Duration(val range: RangeParams) : RecordsFilterParam + @Parcelize + data class Duplications(val items: List) : RecordsFilterParam + sealed interface CommentItem : Parcelable { @Parcelize object NoComment : CommentItem @@ -68,4 +71,12 @@ sealed interface RecordsFilterParam : Parcelable { @Parcelize object Untagged : TagItem } + + sealed interface DuplicationsItem : Parcelable { + @Parcelize + object SameActivity : DuplicationsItem + + @Parcelize + object SameTimes : DuplicationsItem + } } \ No newline at end of file diff --git a/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/RecordsFilterParams.kt b/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/RecordsFilterParams.kt index d42eab8af..6f2d56b91 100644 --- a/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/RecordsFilterParams.kt +++ b/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/RecordsFilterParams.kt @@ -7,22 +7,31 @@ import kotlinx.parcelize.Parcelize data class RecordsFilterParams( val tag: String, val title: String, - val dateSelectionAvailable: Boolean, - val untrackedSelectionAvailable: Boolean, - val multitaskSelectionAvailable: Boolean, - val addRunningRecords: Boolean, + val flags: Flags, val filters: List, val defaultLastDaysNumber: Int, ) : ScreenParams, Parcelable { + @Parcelize + data class Flags( + val dateSelectionAvailable: Boolean, + val untrackedSelectionAvailable: Boolean, + val multitaskSelectionAvailable: Boolean, + val duplicationsSelectionAvailable: Boolean, + val addRunningRecords: Boolean, + ) : Parcelable + companion object { val Empty = RecordsFilterParams( tag = "", title = "", - dateSelectionAvailable = true, - untrackedSelectionAvailable = true, - multitaskSelectionAvailable = true, - addRunningRecords = true, + flags = Flags( + dateSelectionAvailable = true, + untrackedSelectionAvailable = true, + multitaskSelectionAvailable = true, + duplicationsSelectionAvailable = true, + addRunningRecords = true, + ), filters = emptyList(), defaultLastDaysNumber = 0, ) diff --git a/resources/src/main/res/values-ar/strings.xml b/resources/src/main/res/values-ar/strings.xml index df0ff2718..ced5c1eb8 100644 --- a/resources/src/main/res/values-ar/strings.xml +++ b/resources/src/main/res/values-ar/strings.xml @@ -542,6 +542,7 @@ تم تغيير البيانات + الازدواجية اختيار الوسوم استبعاد العلامات تمت التصفية @@ -551,6 +552,9 @@ عكس الاختيار الحد الأدنى الحد الأقصى + نفس النشاط + نفس الوقت + تصفية التكرارات التركيز diff --git a/resources/src/main/res/values-ca/strings.xml b/resources/src/main/res/values-ca/strings.xml index ba5fb78dd..40129df40 100644 --- a/resources/src/main/res/values-ca/strings.xml +++ b/resources/src/main/res/values-ca/strings.xml @@ -542,6 +542,7 @@ Exemple:
Dades canviades + Duplicacions Seleccioneu etiquetes Exclou etiquetes Filtrat @@ -551,6 +552,9 @@ Exemple:
Inverteix la selecció Mínim Màxim + La mateixa activitat + El mateix temps + Filtra els duplicats Enfocament diff --git a/resources/src/main/res/values-de/strings.xml b/resources/src/main/res/values-de/strings.xml index bfced2284..11a777a20 100644 --- a/resources/src/main/res/values-de/strings.xml +++ b/resources/src/main/res/values-de/strings.xml @@ -542,6 +542,7 @@ Beispiel:
Daten geändert + Vervielfältigungen Tags auswählen Tags ausschließen Gefiltert @@ -551,6 +552,9 @@ Beispiel:
Auswahl umkehren Minimum Maximal + Gleiche Aktivität + Zur gleichen Zeit + Duplikate filtern Fokus diff --git a/resources/src/main/res/values-es/strings.xml b/resources/src/main/res/values-es/strings.xml index 71a1bdc94..ec62a76cb 100644 --- a/resources/src/main/res/values-es/strings.xml +++ b/resources/src/main/res/values-es/strings.xml @@ -542,6 +542,7 @@ Ejemplo:
Datos cambiados + Duplicaciones Seleccionar etiquetas Excluir etiquetas Filtrada @@ -551,6 +552,9 @@ Ejemplo:
Invertir selección Mínima Máxima + Misma actividad + Mismo tiempo + Filtrar duplicados Enfoque diff --git a/resources/src/main/res/values-fa/strings.xml b/resources/src/main/res/values-fa/strings.xml index a37f0f4a2..289547e16 100644 --- a/resources/src/main/res/values-fa/strings.xml +++ b/resources/src/main/res/values-fa/strings.xml @@ -542,6 +542,7 @@ اطلاعات تغییر کردند + تکراری ها انتخاب برچسب ها حذف برچسب ها فیلتر شده @@ -551,6 +552,9 @@ معکوس کردن موارد انتخابی حداقل حداکثر + همان فعالیت + همان زمان + فیلترهای تکراری تمرکز diff --git a/resources/src/main/res/values-fr/strings.xml b/resources/src/main/res/values-fr/strings.xml index a738c63dc..1ff785d33 100644 --- a/resources/src/main/res/values-fr/strings.xml +++ b/resources/src/main/res/values-fr/strings.xml @@ -542,6 +542,7 @@ Exemple:
Données modifiées + Duplications Sélectionnez les balises Exclure les balises Filtrée @@ -551,6 +552,9 @@ Exemple:
Inverser la sélection Minimum Maximum + Même activité + En même temps + Filtrer les doublons Concentration diff --git a/resources/src/main/res/values-hi/strings.xml b/resources/src/main/res/values-hi/strings.xml index 3249bc9dd..1309d48c8 100644 --- a/resources/src/main/res/values-hi/strings.xml +++ b/resources/src/main/res/values-hi/strings.xml @@ -542,6 +542,7 @@ csv फ़ाइल में कॉमा से अलग किए गए य डेटा बदल गया + दोहराव टैग चुनें टैग हटाएं छाना हुआ @@ -551,6 +552,9 @@ csv फ़ाइल में कॉमा से अलग किए गए य उलट चयन न्यूनतम अधिकतम + वही गतिविधि + उसी समय + डुप्लिकेट फ़िल्टर करें फोकस diff --git a/resources/src/main/res/values-in/strings.xml b/resources/src/main/res/values-in/strings.xml index 3560a8acb..4c6d681c1 100644 --- a/resources/src/main/res/values-in/strings.xml +++ b/resources/src/main/res/values-in/strings.xml @@ -542,6 +542,7 @@ Contoh:
Data berubah + Duplikasi Pilih tag Kecualikan tag Tersaring @@ -551,6 +552,9 @@ Contoh:
Pilihan sebaliknya Minimum Maksimum + Aktivitas yang sama + Waktu yang sama + Filter duplikat Fokus diff --git a/resources/src/main/res/values-it/strings.xml b/resources/src/main/res/values-it/strings.xml index 6c41be53f..ee6acd8eb 100644 --- a/resources/src/main/res/values-it/strings.xml +++ b/resources/src/main/res/values-it/strings.xml @@ -542,6 +542,7 @@ Esempio:
Dati modificati + Duplicazioni Seleziona i tag Escludi tag Filtrata @@ -551,6 +552,9 @@ Esempio:
Inverti selezione Minimo Massimo + Stessa attività + Allo stesso tempo + Filtra i duplicati Focus diff --git a/resources/src/main/res/values-iw/strings.xml b/resources/src/main/res/values-iw/strings.xml index 5fc97a8e8..43f7b4e9f 100644 --- a/resources/src/main/res/values-iw/strings.xml +++ b/resources/src/main/res/values-iw/strings.xml @@ -542,6 +542,7 @@ נתונים שונו + כפילויות בחר תגיות החרג תגיות מסונן @@ -551,6 +552,9 @@ הפוך בחירה מִינִימוּם מַקסִימוּם + אותה פעילות + אותו זמן + סנן כפילויות פוקוס diff --git a/resources/src/main/res/values-ja/strings.xml b/resources/src/main/res/values-ja/strings.xml index 81f21aad7..59d91e7a5 100644 --- a/resources/src/main/res/values-ja/strings.xml +++ b/resources/src/main/res/values-ja/strings.xml @@ -542,6 +542,7 @@ CSV ファイルには、カンマで区切られた次の列が含まれてい データが変更されました + 重複 タグを選択 タグを除外する フィルタリングされた @@ -551,6 +552,9 @@ CSV ファイルには、カンマで区切られた次の列が含まれてい 選択範囲を反転する 最小 最大 + 同じアクティビティ + 同じ時間 + 重複をフィルタリングする フォーカス diff --git a/resources/src/main/res/values-ko/strings.xml b/resources/src/main/res/values-ko/strings.xml index 53c6c6582..bca7192b4 100644 --- a/resources/src/main/res/values-ko/strings.xml +++ b/resources/src/main/res/values-ko/strings.xml @@ -542,6 +542,7 @@ csv 파일은 다음과 같은 열(column)들을 가져야합니다:
데이터 변경됨 + 중복 태그 선택 태그 제외 필터링됨 @@ -551,6 +552,9 @@ csv 파일은 다음과 같은 열(column)들을 가져야합니다:
선택 반전 최저한의 최고 + 동일한 활동 + 같은 시간 + 중복 필터링 초점 diff --git a/resources/src/main/res/values-nl/strings.xml b/resources/src/main/res/values-nl/strings.xml index 463b1225e..6f05699ec 100644 --- a/resources/src/main/res/values-nl/strings.xml +++ b/resources/src/main/res/values-nl/strings.xml @@ -542,6 +542,7 @@ Voorbeeld:
Gegevens gewijzigd + Duplicaties Selecteer labels Labels uitsluiten Gefilterd @@ -551,6 +552,9 @@ Voorbeeld:
Omgekeerde selectie Minimum Maximaal + Dezelfde activiteit + Zelfde tijd + Filter duplicaten Focus diff --git a/resources/src/main/res/values-pl/strings.xml b/resources/src/main/res/values-pl/strings.xml index 8226009c0..5b93cbed1 100644 --- a/resources/src/main/res/values-pl/strings.xml +++ b/resources/src/main/res/values-pl/strings.xml @@ -542,6 +542,7 @@ Przykład:
Nadpisano + Duplikacje Wybierz tagi Wyklucz tagi Odfiltrowane @@ -551,6 +552,9 @@ Przykład:
Odwróć wybór Minimum Maksymalny + Ta sama aktywność + W tym samym czasie + Filtruj duplikaty Skupienie diff --git a/resources/src/main/res/values-pt-rPT/strings.xml b/resources/src/main/res/values-pt-rPT/strings.xml index 5d864ca65..c25d99d87 100644 --- a/resources/src/main/res/values-pt-rPT/strings.xml +++ b/resources/src/main/res/values-pt-rPT/strings.xml @@ -542,6 +542,7 @@ Exemplo:
Dados alterados + Duplicações Selecionar etiquetas Excluir etiquetas Filtração @@ -551,6 +552,9 @@ Exemplo:
Seleção invertida Mínima Máxima + Mesma atividade + Mesma hora + Filtrar duplicados Foco diff --git a/resources/src/main/res/values-pt/strings.xml b/resources/src/main/res/values-pt/strings.xml index e6069799e..c45a21bb8 100644 --- a/resources/src/main/res/values-pt/strings.xml +++ b/resources/src/main/res/values-pt/strings.xml @@ -542,6 +542,7 @@ Exemplo:
Dados alterados + Duplicações Selecionar tags Excluir tags Filtrada @@ -551,6 +552,9 @@ Exemplo:
Seleção invertida Mínima Máxima + Mesma atividade + Mesma hora + Filtrar duplicatas Foco diff --git a/resources/src/main/res/values-ro/strings.xml b/resources/src/main/res/values-ro/strings.xml index 0aac3e8a6..95690dc49 100644 --- a/resources/src/main/res/values-ro/strings.xml +++ b/resources/src/main/res/values-ro/strings.xml @@ -542,6 +542,7 @@ Examplu:
Informații modificate + Duplicări Alege etichetele Exclude etichete Filtrate @@ -551,6 +552,9 @@ Examplu:
Inversează selecția Minim Maxim + Aceeași activitate + Același timp + Filtrați duplicatele Concentrare diff --git a/resources/src/main/res/values-ru/strings.xml b/resources/src/main/res/values-ru/strings.xml index c718a1664..7257f8686 100644 --- a/resources/src/main/res/values-ru/strings.xml +++ b/resources/src/main/res/values-ru/strings.xml @@ -542,6 +542,7 @@ CSV-файл должен содержать следующие столбцы, Данные изменены + Дублирование Выбрать теги Исключить теги Отфильтровано @@ -551,6 +552,9 @@ CSV-файл должен содержать следующие столбцы, Инвертировать выбор Минимум Максимум + Та же деятельность + В то же время + Фильтровать дубликаты Фокус diff --git a/resources/src/main/res/values-sv/strings.xml b/resources/src/main/res/values-sv/strings.xml index 1279a53f0..8dc405473 100644 --- a/resources/src/main/res/values-sv/strings.xml +++ b/resources/src/main/res/values-sv/strings.xml @@ -542,6 +542,7 @@ Exempel:
Data har ändrats + Dupliceringar Välj taggar Uteslut taggar Filtrerad @@ -551,6 +552,9 @@ Exempel:
Invertera urval Minimum Maximal + Samma aktivitet + Samma tid + Filtrera dubbletter Fokus diff --git a/resources/src/main/res/values-tr/strings.xml b/resources/src/main/res/values-tr/strings.xml index 3a8b78dc2..33c2985cf 100644 --- a/resources/src/main/res/values-tr/strings.xml +++ b/resources/src/main/res/values-tr/strings.xml @@ -542,6 +542,7 @@ CSV dosyası virgülle ayrılmış şu sütunları içermelidir:
Veri değişti + Çoğaltmalar Etiketleri seçin Etiketleri hariç tut Filtrelendi @@ -551,6 +552,9 @@ CSV dosyası virgülle ayrılmış şu sütunları içermelidir:
Zıt seçim Asgari Maksimum + Aynı aktivite + Aynı zaman + Kopyaları filtrele Odaklanma diff --git a/resources/src/main/res/values-uk/strings.xml b/resources/src/main/res/values-uk/strings.xml index e7b351d41..caaa9defc 100644 --- a/resources/src/main/res/values-uk/strings.xml +++ b/resources/src/main/res/values-uk/strings.xml @@ -542,6 +542,7 @@ Дані змінено + Дублювання Вибрати теги Виключити теги Відфільтрований @@ -551,6 +552,9 @@ Інвертувати виділення Мінімум Максимум + Така сама діяльність + Той самий час + Фільтр дублікатів Фокус diff --git a/resources/src/main/res/values-vi/strings.xml b/resources/src/main/res/values-vi/strings.xml index 85432c9c6..db5e67dea 100644 --- a/resources/src/main/res/values-vi/strings.xml +++ b/resources/src/main/res/values-vi/strings.xml @@ -542,6 +542,7 @@ Ví dụ:
Dữ liệu đã thay đổi + Bản sao Chọn thẻ Loại trừ thẻ Đã lọc @@ -551,6 +552,9 @@ Ví dụ:
Đảo ngược lựa chọn Tối thiểu Tối đa + Hoạt động tương tự + Cùng thời gian + Lọc trùng lặp Tiêu điểm diff --git a/resources/src/main/res/values-zh-rTW/strings.xml b/resources/src/main/res/values-zh-rTW/strings.xml index 30fe99745..f5d11756b 100644 --- a/resources/src/main/res/values-zh-rTW/strings.xml +++ b/resources/src/main/res/values-zh-rTW/strings.xml @@ -542,6 +542,7 @@ csv 文件必須包含以逗號分隔的這些列:
數據已更改 + 重複 選擇標籤 排除標籤 已過濾 @@ -551,6 +552,9 @@ csv 文件必須包含以逗號分隔的這些列:
反轉選擇 最低限度 最大限度 + 相同的活動 + 同一時間 + 過濾重複項 焦點 diff --git a/resources/src/main/res/values-zh/strings.xml b/resources/src/main/res/values-zh/strings.xml index e4baa0d83..a5999d591 100644 --- a/resources/src/main/res/values-zh/strings.xml +++ b/resources/src/main/res/values-zh/strings.xml @@ -542,6 +542,7 @@ csv 文件必须包含以逗号分隔的这些列:
数据已更改 + 重复 选择标签 排除标签 已过滤 @@ -551,6 +552,9 @@ csv 文件必须包含以逗号分隔的这些列:
反转选择 最低限度 最大限度 + 相同的活动 + 同一时间 + 过滤重复项 焦点 diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index bb3063618..f4dcd7fe2 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -542,6 +542,7 @@ Example:
Data changed + Duplications Select tags Exclude tags Filtered @@ -551,6 +552,9 @@ Example:
Invert selection Minimum Maximum + Same activity + Same time + Filter duplicates Focus