diff --git a/README.md b/README.md index e875f92b5..ce807da1f 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Simple app that helps track how much time you spend on all the useless activitie │ ├── feature_settings # One of main tabs, settings. │ ├── feature_statistics # One of main tabs, statistics. │ ├── feature_statistics_detail # Screen showing detailed statistics. + │ ├── feature_suggestions # Screen for activity suggestions. │ ├── feature_tag_selection # Screen for selecting tags. │ ├── feature_views # Custom views. │ ├── feature_wear # Phone app logic to connect to wear app. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 981b3cb3f..c24895211 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -109,6 +109,7 @@ dependencies { implementation(project(":feature_goals")) implementation(project(":feature_pomodoro")) implementation(project(":feature_complex_rules")) + implementation(project(":feature_suggestions")) implementation(project(":feature_change_complex_rule")) implementation(project(":feature_change_goals")) implementation(project(":feature_change_goals:api")) diff --git a/app/src/main/java/com/example/util/simpletimetracker/di/NavigationScreenMapModule.kt b/app/src/main/java/com/example/util/simpletimetracker/di/NavigationScreenMapModule.kt index eb97d5b48..11d35208d 100644 --- a/app/src/main/java/com/example/util/simpletimetracker/di/NavigationScreenMapModule.kt +++ b/app/src/main/java/com/example/util/simpletimetracker/di/NavigationScreenMapModule.kt @@ -13,6 +13,7 @@ import com.example.util.simpletimetracker.feature_statistics_detail.view.Statist import com.example.util.simpletimetracker.navigation.NavigationData import com.example.util.simpletimetracker.navigation.bundleCreator.BundleCreator import com.example.util.simpletimetracker.navigation.bundleCreator.bundleCreatorDelegate +import com.example.util.simpletimetracker.navigation.params.screen.ActivitySuggestionsParams import com.example.util.simpletimetracker.navigation.params.screen.ArchiveParams import com.example.util.simpletimetracker.navigation.params.screen.CategoriesParams import com.example.util.simpletimetracker.navigation.params.screen.ChangeActivityFilterParams @@ -162,6 +163,16 @@ class NavigationScreenMapModule { ) } + @IntoMap + @Provides + @ScreenKey(ActivitySuggestionsParams::class) + fun activitySuggestions(): NavigationData { + return NavigationData( + R.id.action_to_activitySuggestionsFragment, + BundleCreator.empty(), + ) + } + @IntoMap @Provides @ScreenKey(ChangeCategoryFromTagsParams::class) diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 02e9b14e6..dae422bc9 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -37,6 +37,14 @@ app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> + + + , records: List, + maxCount: Int, ): Map> { val counts = mutableMapOf>() val recordsSorted = records.sortedBy { it.timeStarted } @@ -40,7 +41,7 @@ class CalculateAdjacentActivitiesInteractor @Inject constructor() { return counts.mapValues { (_, counts) -> counts.keys .sortedByDescending { counts[it].orZero() } - .take(MAX_COUNT) + .take(maxCount) .map { CalculationResult(it, counts[it].orZero()) } } } @@ -49,6 +50,7 @@ class CalculateAdjacentActivitiesInteractor @Inject constructor() { fun calculateMultitasking( typeId: Long, records: List, + maxCount: Int, ): List { val counts = mutableMapOf() @@ -76,7 +78,7 @@ class CalculateAdjacentActivitiesInteractor @Inject constructor() { return counts.keys .sortedByDescending { counts[it].orZero() } - .take(MAX_COUNT) + .take(maxCount) .map { CalculationResult(it, counts[it].orZero()) } } @@ -84,8 +86,4 @@ class CalculateAdjacentActivitiesInteractor @Inject constructor() { val typeId: Long, val count: Long, ) - - companion object { - private const val MAX_COUNT = 5 - } } \ No newline at end of file diff --git a/domain/src/test/java/com/example/util/simpletimetracker/domain/mapper/CalculateAdjacentActivitiesInteractorTest.kt b/domain/src/test/java/com/example/util/simpletimetracker/domain/mapper/CalculateAdjacentActivitiesInteractorTest.kt index b9fd7e5b9..ee8300046 100644 --- a/domain/src/test/java/com/example/util/simpletimetracker/domain/mapper/CalculateAdjacentActivitiesInteractorTest.kt +++ b/domain/src/test/java/com/example/util/simpletimetracker/domain/mapper/CalculateAdjacentActivitiesInteractorTest.kt @@ -23,6 +23,7 @@ class CalculateAdjacentActivitiesInteractorTest( val actual = subject.calculateNextActivities( typeIds = input.first, records = input.second, + maxCount = 5, ) assertEquals( diff --git a/features/feature_base_adapter/src/main/java/com/example/util/simpletimetracker/feature_base_adapter/hint/HintAdapterDelegate.kt b/features/feature_base_adapter/src/main/java/com/example/util/simpletimetracker/feature_base_adapter/hint/HintAdapterDelegate.kt index fc260e06f..e278a1834 100644 --- a/features/feature_base_adapter/src/main/java/com/example/util/simpletimetracker/feature_base_adapter/hint/HintAdapterDelegate.kt +++ b/features/feature_base_adapter/src/main/java/com/example/util/simpletimetracker/feature_base_adapter/hint/HintAdapterDelegate.kt @@ -1,5 +1,6 @@ package com.example.util.simpletimetracker.feature_base_adapter.hint +import android.view.Gravity import androidx.core.view.updatePadding import com.example.util.simpletimetracker.feature_base_adapter.createRecyclerBindingAdapterDelegate import com.example.util.simpletimetracker.feature_views.extension.dpToPx @@ -10,6 +11,13 @@ fun createHintAdapterDelegate() = createRecyclerBindingAdapterDelegate + fun ViewData.Gravity.toViewData(): Int { + return when (this) { + ViewData.Gravity.CENTER -> Gravity.CENTER + ViewData.Gravity.START -> Gravity.START + } + } + with(binding) { item as ViewData @@ -18,5 +26,6 @@ fun createHintAdapterDelegate() = createRecyclerBindingAdapterDelegate - params.width = item.width.dpToPx() - params.height = item.height.dpToPx() + item.width?.dpToPx()?.let { params.width = it } + item.height?.dpToPx()?.let { params.height = it } } itemIsRow = item.asRow diff --git a/features/feature_base_adapter/src/main/java/com/example/util/simpletimetracker/feature_base_adapter/recordTypeSpecial/RunningRecordTypeSpecialViewData.kt b/features/feature_base_adapter/src/main/java/com/example/util/simpletimetracker/feature_base_adapter/recordTypeSpecial/RunningRecordTypeSpecialViewData.kt index 641e3a432..4b850c772 100644 --- a/features/feature_base_adapter/src/main/java/com/example/util/simpletimetracker/feature_base_adapter/recordTypeSpecial/RunningRecordTypeSpecialViewData.kt +++ b/features/feature_base_adapter/src/main/java/com/example/util/simpletimetracker/feature_base_adapter/recordTypeSpecial/RunningRecordTypeSpecialViewData.kt @@ -10,8 +10,8 @@ data class RunningRecordTypeSpecialViewData( val name: String, val iconId: RecordTypeIcon, @ColorInt val color: Int, - val width: Int, - val height: Int, + val width: Int?, + val height: Int?, val asRow: Boolean = false, val checkState: CheckState = CheckState.HIDDEN, ) : ViewHolderType { diff --git a/features/feature_change_record/src/main/res/layout/change_record_core_layout.xml b/features/feature_change_record/src/main/res/layout/change_record_core_layout.xml index 2ccefccbc..e4ae21adf 100644 --- a/features/feature_change_record/src/main/res/layout/change_record_core_layout.xml +++ b/features/feature_change_record/src/main/res/layout/change_record_core_layout.xml @@ -14,7 +14,7 @@ app:layout_constraintBottom_toTopOf="@id/dividerChangeRecordButton" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0" - tools:visibility="gone"> + tools:visibility="visible"> + tools:visibility="gone" /> - diff --git a/features/feature_dialogs/src/main/java/com/example/util/simpletimetracker/feature_dialogs/typesSelection/viewModel/TypesSelectionViewModel.kt b/features/feature_dialogs/src/main/java/com/example/util/simpletimetracker/feature_dialogs/typesSelection/viewModel/TypesSelectionViewModel.kt index 6d104f62c..0ea2e21c1 100644 --- a/features/feature_dialogs/src/main/java/com/example/util/simpletimetracker/feature_dialogs/typesSelection/viewModel/TypesSelectionViewModel.kt +++ b/features/feature_dialogs/src/main/java/com/example/util/simpletimetracker/feature_dialogs/typesSelection/viewModel/TypesSelectionViewModel.kt @@ -67,6 +67,7 @@ class TypesSelectionViewModel @Inject constructor( } fun onShowAllClick() { + dataIdsSelected.clear() dataIdsSelected.addAll(viewDataCache.map(TypesSelectionCacheHolder::id)) updateViewData() } diff --git a/features/feature_settings/api/src/main/java/com/example/util/simpletimetracker/feature_settings/api/SettingsBlock.kt b/features/feature_settings/api/src/main/java/com/example/util/simpletimetracker/feature_settings/api/SettingsBlock.kt index 43cbcfa0d..95cc42ea6 100644 --- a/features/feature_settings/api/src/main/java/com/example/util/simpletimetracker/feature_settings/api/SettingsBlock.kt +++ b/features/feature_settings/api/src/main/java/com/example/util/simpletimetracker/feature_settings/api/SettingsBlock.kt @@ -84,6 +84,7 @@ enum class SettingsBlock { AdditionalSendEvents, AdditionalDataEdit, AdditionalComplexRules, + AdditionalActivitySuggestions, AdditionalBottom, BackupTop, diff --git a/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/interactor/SettingsAdditionalViewDataInteractor.kt b/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/interactor/SettingsAdditionalViewDataInteractor.kt index dc0c91684..efd55c46c 100644 --- a/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/interactor/SettingsAdditionalViewDataInteractor.kt +++ b/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/interactor/SettingsAdditionalViewDataInteractor.kt @@ -182,6 +182,11 @@ class SettingsAdditionalViewDataInteractor @Inject constructor( block = SettingsBlock.AdditionalComplexRules, title = resourceRepo.getString(R.string.settings_complex_rules), subtitle = "", + ) + result += SettingsTextViewData( + block = SettingsBlock.AdditionalActivitySuggestions, + title = resourceRepo.getString(R.string.settings_activity_suggestions), + subtitle = "", dividerIsVisible = false, ) } diff --git a/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/viewModel/delegate/SettingsAdditionalViewModelDelegate.kt b/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/viewModel/delegate/SettingsAdditionalViewModelDelegate.kt index 9dd756f21..b00590537 100644 --- a/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/viewModel/delegate/SettingsAdditionalViewModelDelegate.kt +++ b/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/viewModel/delegate/SettingsAdditionalViewModelDelegate.kt @@ -18,6 +18,7 @@ import com.example.util.simpletimetracker.feature_settings.mapper.SettingsAutoma import com.example.util.simpletimetracker.feature_settings.mapper.SettingsMapper import com.example.util.simpletimetracker.feature_settings.viewModel.SettingsViewModel import com.example.util.simpletimetracker.navigation.Router +import com.example.util.simpletimetracker.navigation.params.screen.ActivitySuggestionsParams import com.example.util.simpletimetracker.navigation.params.screen.ComplexRulesParams import com.example.util.simpletimetracker.navigation.params.screen.DataEditParams import com.example.util.simpletimetracker.navigation.params.screen.DurationDialogParams @@ -71,6 +72,7 @@ class SettingsAdditionalViewModelDelegate @Inject constructor( SettingsBlock.AdditionalKeepScreenOn -> onKeepScreenOnClicked() SettingsBlock.AdditionalDataEdit -> onDataEditClick() SettingsBlock.AdditionalComplexRules -> onComplexRulesClick() + SettingsBlock.AdditionalActivitySuggestions -> onActivitySuggestionsClick() else -> { // Do nothing } @@ -262,6 +264,10 @@ class SettingsAdditionalViewModelDelegate @Inject constructor( router.navigate(ComplexRulesParams) } + private fun onActivitySuggestionsClick() { + router.navigate(ActivitySuggestionsParams) + } + private fun onAutomatedTrackingHelpClick() { delegateScope.launch { val isDarkTheme = prefsInteractor.getDarkMode() diff --git a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt index 96bebfbbd..e2e446965 100644 --- a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt +++ b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt @@ -41,10 +41,10 @@ class StatisticsDetailAdjacentActivitiesInteractor @Inject constructor( val recordTypes = recordTypeInteractor.getAll().associateBy(RecordType::id) val actualRecords = getRecords(rangeLength, rangePosition) val nextActivitiesIds = calculateAdjacentActivitiesInteractor - .calculateNextActivities(listOf(typeId), actualRecords) + .calculateNextActivities(listOf(typeId), actualRecords, MAX_COUNT) .getOrElse(typeId) { emptyList() } val multitaskingActivitiesIds = calculateAdjacentActivitiesInteractor - .calculateMultitasking(typeId, actualRecords) + .calculateMultitasking(typeId, actualRecords, MAX_COUNT) fun mapPreviews(typeToCounts: List): List { val total = typeToCounts.sumOf(CalculationResult::count) @@ -129,4 +129,8 @@ class StatisticsDetailAdjacentActivitiesInteractor @Inject constructor( private fun getEmptyViewData(): List { return emptyList() } + + companion object { + private const val MAX_COUNT = 5 + } } \ No newline at end of file diff --git a/features/feature_suggestions/.gitignore b/features/feature_suggestions/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/features/feature_suggestions/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/feature_suggestions/build.gradle.kts b/features/feature_suggestions/build.gradle.kts new file mode 100644 index 000000000..0c18818b5 --- /dev/null +++ b/features/feature_suggestions/build.gradle.kts @@ -0,0 +1,21 @@ +import com.example.util.simpletimetracker.Base +import com.example.util.simpletimetracker.applyAndroidLibrary + +plugins { + alias(libs.plugins.gradleLibrary) + alias(libs.plugins.kotlin) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) +} + +applyAndroidLibrary() + +android { + namespace = "${Base.namespace}.feature_suggestions" +} + +dependencies { + implementation(project(":core")) + implementation(libs.google.dagger) + ksp(libs.kapt.dagger) +} diff --git a/features/feature_suggestions/src/main/AndroidManifest.xml b/features/feature_suggestions/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7726109eb --- /dev/null +++ b/features/feature_suggestions/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivitySuggestionAdapterDelegate.kt b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivitySuggestionAdapterDelegate.kt new file mode 100644 index 000000000..7024f1c69 --- /dev/null +++ b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivitySuggestionAdapterDelegate.kt @@ -0,0 +1,40 @@ +package com.example.util.simpletimetracker.feature_suggestions.adapter + +import androidx.annotation.ColorInt +import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType +import com.example.util.simpletimetracker.feature_base_adapter.createRecyclerBindingAdapterDelegate +import com.example.util.simpletimetracker.feature_views.viewData.RecordTypeIcon +import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionViewData as ViewData +import com.example.util.simpletimetracker.feature_suggestions.databinding.ItemActivitySuggestionLayoutBinding as Binding + +fun createActivitySuggestionAdapterDelegate() = createRecyclerBindingAdapterDelegate( + Binding::inflate, +) { binding, item, _ -> + + with(binding.viewActivitySuggestionItem) { + item as ViewData + + itemColor = item.color + itemIcon = item.iconId + itemIconColor = item.iconColor + itemName = item.name + } +} + +data class ActivitySuggestionViewData( + val id: Id, + val name: String, + val iconId: RecordTypeIcon, + @ColorInt val iconColor: Int, + @ColorInt val color: Int, +) : ViewHolderType { + + override fun getUniqueId(): Long = id.hashCode().toLong() + + override fun isValidType(other: ViewHolderType): Boolean = other is ViewData + + data class Id( + val suggestionTypeId: Long, + val forTypeId: Long, + ) +} \ No newline at end of file diff --git a/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivitySuggestionSpecialAdapterDelegate.kt b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivitySuggestionSpecialAdapterDelegate.kt new file mode 100644 index 000000000..086718c4d --- /dev/null +++ b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivitySuggestionSpecialAdapterDelegate.kt @@ -0,0 +1,47 @@ +package com.example.util.simpletimetracker.feature_suggestions.adapter + +import androidx.annotation.ColorInt +import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType +import com.example.util.simpletimetracker.feature_base_adapter.createRecyclerBindingAdapterDelegate +import com.example.util.simpletimetracker.feature_views.extension.setOnClickWith +import com.example.util.simpletimetracker.feature_views.viewData.RecordTypeIcon +import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionSpecialViewData as ViewData +import com.example.util.simpletimetracker.feature_suggestions.databinding.ItemActivitySuggestionLayoutBinding as Binding + +fun createActivitySuggestionSpecialAdapterDelegate( + onItemClick: ((ViewData) -> Unit), +) = createRecyclerBindingAdapterDelegate( + Binding::inflate, +) { binding, item, _ -> + + with(binding.viewActivitySuggestionItem) { + item as ViewData + + itemColor = item.color + itemIcon = item.iconId + itemName = item.name + setOnClickWith(item, onItemClick) + } +} + +data class ActivitySuggestionSpecialViewData( + val id: Id, + val name: String, + val iconId: RecordTypeIcon, + @ColorInt val color: Int, +) : ViewHolderType { + + override fun getUniqueId(): Long = id.hashCode().toLong() + + override fun isValidType(other: ViewHolderType): Boolean = other is ViewData + + data class Id( + val forTypeId: Long, + val type: Type, + ) + + sealed interface Type { + data object Add : Type + data object Calculate : Type + } +} \ No newline at end of file diff --git a/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivityiSuggestionsButtonAdapterDelegate.kt b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivityiSuggestionsButtonAdapterDelegate.kt new file mode 100644 index 000000000..50586130a --- /dev/null +++ b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/adapter/ActivityiSuggestionsButtonAdapterDelegate.kt @@ -0,0 +1,51 @@ +package com.example.util.simpletimetracker.feature_suggestions.adapter + +import android.content.res.ColorStateList +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType +import com.example.util.simpletimetracker.feature_base_adapter.createRecyclerBindingAdapterDelegate +import com.example.util.simpletimetracker.feature_views.extension.setOnClickWith +import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionsButtonViewData as ViewData +import com.example.util.simpletimetracker.feature_suggestions.databinding.ItemActivitySuggestionsButtonBinding as Binding + +// TODO SUG refactor with record quick actions button, and complex rules button. +// TODO SUG remove ripple from icon background if background is transparent. +// TODO SUG change button background color to appInactive color. +fun createActivitySuggestionsButtonAdapterDelegate( + onClick: (ViewData) -> Unit, +) = createRecyclerBindingAdapterDelegate( + Binding::inflate, +) { binding, item, _ -> + + with(binding) { + item as ViewData + + tvActivitySuggestionsButton.text = item.text + ivActivitySuggestionsButton.setImageResource(item.icon) + ivActivitySuggestionsButton.imageTintList = ColorStateList.valueOf(item.iconColor) + cardActivitySuggestionsButton.setCardBackgroundColor(item.iconBackgroundColor) + itemActivitySuggestionsButton.isEnabled = item.isEnabled + itemActivitySuggestionsButton.setOnClickWith(item, onClick) + } +} + +data class ActivitySuggestionsButtonViewData( + val block: Block, + val text: String, + @DrawableRes val icon: Int, + @ColorInt val iconColor: Int, + @ColorInt val iconBackgroundColor: Int, + val isEnabled: Boolean, +) : ViewHolderType { + + override fun getUniqueId(): Long = block.ordinal.toLong() + + override fun isValidType(other: ViewHolderType): Boolean = + other is ViewData + + enum class Block { + ADD, + CALCULATE, + } +} \ No newline at end of file diff --git a/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/interactor/ActivitySuggestionsCalculateInteractor.kt b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/interactor/ActivitySuggestionsCalculateInteractor.kt new file mode 100644 index 000000000..2b8978495 --- /dev/null +++ b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/interactor/ActivitySuggestionsCalculateInteractor.kt @@ -0,0 +1,45 @@ +package com.example.util.simpletimetracker.feature_suggestions.interactor + +import com.example.util.simpletimetracker.core.mapper.TimeMapper +import com.example.util.simpletimetracker.domain.prefs.interactor.PrefsInteractor +import com.example.util.simpletimetracker.domain.record.interactor.CalculateAdjacentActivitiesInteractor +import com.example.util.simpletimetracker.domain.record.interactor.RecordInteractor +import com.example.util.simpletimetracker.domain.statistics.model.RangeLength +import com.example.util.simpletimetracker.feature_suggestions.model.ActivitySuggestionModel +import javax.inject.Inject + +class ActivitySuggestionsCalculateInteractor @Inject constructor( + private val timeMapper: TimeMapper, + private val prefsInteractor: PrefsInteractor, + private val recordInteractor: RecordInteractor, + private val calculateAdjacentActivitiesInteractor: CalculateAdjacentActivitiesInteractor, +) { + + suspend fun execute( + typeIds: List, + ): List { + // TODO SUG selectable range? + val range = timeMapper.getRangeStartAndEnd( + rangeLength = RangeLength.Year, + shift = 0, + firstDayOfWeek = prefsInteractor.getFirstDayOfWeek(), + startOfDayShift = prefsInteractor.getStartOfDayShift(), + ) + val records = recordInteractor.getFromRange(range) + + val data = calculateAdjacentActivitiesInteractor.calculateNextActivities( + typeIds = typeIds, + records = records, + maxCount = Int.MAX_VALUE, + ) + + return typeIds.mapNotNull { typeId -> + val thisTypeSuggestions = data[typeId] + ?: return@mapNotNull null + ActivitySuggestionModel( + typeId = typeId, + suggestions = thisTypeSuggestions.map { it.typeId }, + ) + } + } +} \ No newline at end of file diff --git a/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/interactor/ActivitySuggestionsViewDataInteractor.kt b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/interactor/ActivitySuggestionsViewDataInteractor.kt new file mode 100644 index 000000000..c9e84ca90 --- /dev/null +++ b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/interactor/ActivitySuggestionsViewDataInteractor.kt @@ -0,0 +1,209 @@ +package com.example.util.simpletimetracker.feature_suggestions.interactor + +import com.example.util.simpletimetracker.core.mapper.RecordTypeViewDataMapper +import com.example.util.simpletimetracker.core.repo.ResourceRepo +import com.example.util.simpletimetracker.domain.extension.plusAssign +import com.example.util.simpletimetracker.domain.prefs.interactor.PrefsInteractor +import com.example.util.simpletimetracker.domain.recordType.interactor.RecordTypeInteractor +import com.example.util.simpletimetracker.domain.recordType.model.RecordType +import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType +import com.example.util.simpletimetracker.feature_base_adapter.divider.DividerViewData +import com.example.util.simpletimetracker.feature_base_adapter.emptySpace.EmptySpaceViewData +import com.example.util.simpletimetracker.feature_base_adapter.hint.HintViewData +import com.example.util.simpletimetracker.feature_base_adapter.recordTypeSpecial.RunningRecordTypeSpecialViewData +import com.example.util.simpletimetracker.feature_suggestions.R +import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionSpecialViewData +import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionViewData +import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionsButtonViewData +import com.example.util.simpletimetracker.feature_suggestions.model.ActivitySuggestionModel +import com.example.util.simpletimetracker.feature_views.viewData.RecordTypeIcon +import javax.inject.Inject + +class ActivitySuggestionsViewDataInteractor @Inject constructor( + private val resourceRepo: ResourceRepo, + private val prefsInteractor: PrefsInteractor, + private val recordTypeInteractor: RecordTypeInteractor, + private val recordTypeViewDataMapper: RecordTypeViewDataMapper, +) { + + // TODO SUG translate strings + suspend fun getViewData( + suggestions: List, + ): List { + val isDarkTheme = prefsInteractor.getDarkMode() + val recordTypes = recordTypeInteractor.getAll().filter { !it.hidden } + val typesOrder = recordTypes.map(RecordType::id) + val recordTypesMap = recordTypes.associateBy(RecordType::id) + val selectedActivities = suggestions.map { it.typeId } + val suggestionsMap = suggestions.associateBy(ActivitySuggestionModel::typeId) + + val result: MutableList = mutableListOf() + + result += ActivitySuggestionsButtonViewData( + block = ActivitySuggestionsButtonViewData.Block.ADD, + text = resourceRepo.getString(R.string.change_record_message_choose_type), + icon = R.drawable.action_change_item, + iconColor = resourceRepo.getThemedAttr(R.attr.appLightTextColor, isDarkTheme), + iconBackgroundColor = resourceRepo.getColor(R.color.transparent), + isEnabled = true, + ) + + if (selectedActivities.isNotEmpty()) { + result += ActivitySuggestionsButtonViewData( + block = ActivitySuggestionsButtonViewData.Block.CALCULATE, + text = "Calculate from statistics", // TODO SUG + icon = R.drawable.statistics, + iconColor = resourceRepo.getThemedAttr(R.attr.appLightTextColor, isDarkTheme), + iconBackgroundColor = resourceRepo.getColor(R.color.transparent), + isEnabled = true, + ) + } + + selectedActivities.sortedBy { + typesOrder.indexOf(it).toLong() + }.forEachIndexed { index, typeId -> + if (index == 0) { + result += DividerViewData(id = 0) + } + if (index == 0) { + result += HintViewData( + text = resourceRepo.getString(R.string.change_record_type_field), + paddingTop = 0, + paddingBottom = 0, + gravity = HintViewData.Gravity.START, + ) + } + result += recordTypeViewDataMapper.map( + recordType = recordTypesMap[typeId] ?: return@forEachIndexed, + isDarkTheme = isDarkTheme, + ) + result += EmptySpaceViewData( + id = typeId, + wrapBefore = true, + ) + if (index == 0) { + result += HintViewData( + text = resourceRepo.getString(R.string.settings_activity_suggestions), + paddingTop = 0, + paddingBottom = 0, + gravity = HintViewData.Gravity.START, + ) + } + val thisTypeSuggestions = suggestionsMap[typeId]?.suggestions.orEmpty() + thisTypeSuggestions.forEach { suggestion -> + result += mapSuggestion( + forTypeId = typeId, + suggestionTypeId = suggestion, + recordTypesMap = recordTypesMap, + isDarkTheme = isDarkTheme, + ) + } + result += mapAddSuggestionButton( + forTypeId = typeId, + hasAtLeastOneEntry = thisTypeSuggestions.isNotEmpty(), + isDarkTheme = isDarkTheme, + ) + result += mapToCalculateSuggestionButton( + forTypeId = typeId, + isDarkTheme = isDarkTheme, + ) + if (index < selectedActivities.size - 1) { + result += DividerViewData(id = typeId) + } + } + + return result + } + + private fun mapSuggestion( + forTypeId: Long, + suggestionTypeId: Long, + recordTypesMap: Map, + isDarkTheme: Boolean, + ): ActivitySuggestionViewData? { + return recordTypeViewDataMapper.map( + recordType = recordTypesMap[suggestionTypeId] ?: return null, + isDarkTheme = isDarkTheme, + ).let { + ActivitySuggestionViewData( + id = ActivitySuggestionViewData.Id( + suggestionTypeId = suggestionTypeId, + forTypeId = forTypeId, + ), + name = it.name, + iconId = it.iconId, + iconColor = it.iconColor, + color = it.color, + ) + } + } + + private fun mapAddSuggestionButton( + forTypeId: Long, + hasAtLeastOneEntry: Boolean, + isDarkTheme: Boolean, + ): ActivitySuggestionSpecialViewData { + return mapAddButton( + hasAtLeastOneEntry = hasAtLeastOneEntry, + isDarkTheme = isDarkTheme, + ).let { + ActivitySuggestionSpecialViewData( + id = ActivitySuggestionSpecialViewData.Id( + forTypeId = forTypeId, + type = ActivitySuggestionSpecialViewData.Type.Add, + ), + name = it.name, + iconId = it.iconId, + color = it.color, + ) + } + } + + private fun mapToCalculateSuggestionButton( + forTypeId: Long, + isDarkTheme: Boolean, + ): ActivitySuggestionSpecialViewData { + return recordTypeViewDataMapper.mapToAddItem( + numberOfCards = null, + isDarkTheme = isDarkTheme, + ).copy( + name = resourceRepo.getString(R.string.shortcut_navigation_statistics), + iconId = RecordTypeIcon.Image(R.drawable.statistics), + ).let { + ActivitySuggestionSpecialViewData( + id = ActivitySuggestionSpecialViewData.Id( + forTypeId = forTypeId, + type = ActivitySuggestionSpecialViewData.Type.Calculate, + ), + name = it.name, + iconId = it.iconId, + color = it.color, + ) + } + } + + private fun mapAddButton( + hasAtLeastOneEntry: Boolean, + isDarkTheme: Boolean, + ): RunningRecordTypeSpecialViewData { + val name = if (hasAtLeastOneEntry) { + R.string.data_edit_button_change + } else { + R.string.running_records_add_type + }.let(resourceRepo::getString) + + val iconId = if (hasAtLeastOneEntry) { + R.drawable.action_change_item + } else { + R.drawable.add + }.let(RecordTypeIcon::Image) + + return recordTypeViewDataMapper.mapToAddItem( + numberOfCards = null, + isDarkTheme = isDarkTheme, + ).copy( + name = name, + iconId = iconId, + ) + } +} \ No newline at end of file diff --git a/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/model/ActivitySuggestionModel.kt b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/model/ActivitySuggestionModel.kt new file mode 100644 index 000000000..8cd21d60f --- /dev/null +++ b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/model/ActivitySuggestionModel.kt @@ -0,0 +1,6 @@ +package com.example.util.simpletimetracker.feature_suggestions.model + +data class ActivitySuggestionModel( + val typeId: Long, + val suggestions: List, +) \ No newline at end of file diff --git a/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/view/ActivitySuggestionsFragment.kt b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/view/ActivitySuggestionsFragment.kt new file mode 100644 index 000000000..63a3ee708 --- /dev/null +++ b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/view/ActivitySuggestionsFragment.kt @@ -0,0 +1,76 @@ +package com.example.util.simpletimetracker.feature_suggestions.view + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import com.example.util.simpletimetracker.core.base.BaseFragment +import com.example.util.simpletimetracker.core.dialog.TypesSelectionDialogListener +import com.example.util.simpletimetracker.core.utils.InsetConfiguration +import com.example.util.simpletimetracker.feature_base_adapter.BaseRecyclerAdapter +import com.example.util.simpletimetracker.feature_base_adapter.divider.createDividerAdapterDelegate +import com.example.util.simpletimetracker.feature_base_adapter.emptySpace.createEmptySpaceAdapterDelegate +import com.example.util.simpletimetracker.feature_base_adapter.hint.createHintAdapterDelegate +import com.example.util.simpletimetracker.feature_base_adapter.loader.createLoaderAdapterDelegate +import com.example.util.simpletimetracker.feature_base_adapter.recordType.createRecordTypeAdapterDelegate +import com.example.util.simpletimetracker.feature_suggestions.adapter.createActivitySuggestionAdapterDelegate +import com.example.util.simpletimetracker.feature_suggestions.adapter.createActivitySuggestionSpecialAdapterDelegate +import com.example.util.simpletimetracker.feature_suggestions.adapter.createActivitySuggestionsButtonAdapterDelegate +import com.example.util.simpletimetracker.feature_suggestions.viewModel.ActivitySuggestionsViewModel +import com.example.util.simpletimetracker.feature_views.extension.setOnClick +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexWrap +import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.flexbox.JustifyContent +import dagger.hilt.android.AndroidEntryPoint +import com.example.util.simpletimetracker.feature_suggestions.databinding.ActivitySuggestionsFragmentBinding as Binding + +@AndroidEntryPoint +class ActivitySuggestionsFragment : + BaseFragment(), + TypesSelectionDialogListener { + + override val inflater: (LayoutInflater, ViewGroup?, Boolean) -> Binding = + Binding::inflate + + override var insetConfiguration: InsetConfiguration = + InsetConfiguration.ApplyToView { binding.root } + + private val viewModel: ActivitySuggestionsViewModel by viewModels() + + private val viewDataAdapter: BaseRecyclerAdapter by lazy { + BaseRecyclerAdapter( + createDividerAdapterDelegate(), + createEmptySpaceAdapterDelegate(), + createLoaderAdapterDelegate(), + createHintAdapterDelegate(), + createRecordTypeAdapterDelegate(), + createActivitySuggestionAdapterDelegate(), + createActivitySuggestionSpecialAdapterDelegate(throttle(viewModel::onSpecialSuggestionClick)), + createActivitySuggestionsButtonAdapterDelegate(throttle(viewModel::onItemButtonClick)), + ) + } + + override fun initUi(): Unit = with(binding) { + rvActivitySuggestionsList.apply { + layoutManager = FlexboxLayoutManager(requireContext()).apply { + flexDirection = FlexDirection.ROW + justifyContent = JustifyContent.FLEX_START + flexWrap = FlexWrap.WRAP + } + adapter = viewDataAdapter + setHasFixedSize(true) + } + } + + override fun initUx() = with(binding) { + btnActivitySuggestionsSave.setOnClick(throttle(viewModel::onSaveClick)) + } + + override fun initViewModel(): Unit = with(viewModel) { + viewData.observe(viewDataAdapter::replace) + } + + override fun onDataSelected(dataIds: List, tag: String?) { + viewModel.onTypesSelected(dataIds, tag) + } +} diff --git a/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/viewModel/ActivitySuggestionsViewModel.kt b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/viewModel/ActivitySuggestionsViewModel.kt new file mode 100644 index 000000000..35b2db010 --- /dev/null +++ b/features/feature_suggestions/src/main/java/com/example/util/simpletimetracker/feature_suggestions/viewModel/ActivitySuggestionsViewModel.kt @@ -0,0 +1,148 @@ +package com.example.util.simpletimetracker.feature_suggestions.viewModel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.viewModelScope +import com.example.util.simpletimetracker.core.base.BaseViewModel +import com.example.util.simpletimetracker.core.extension.lazySuspend +import com.example.util.simpletimetracker.core.extension.set +import com.example.util.simpletimetracker.core.repo.ResourceRepo +import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType +import com.example.util.simpletimetracker.feature_base_adapter.loader.LoaderViewData +import com.example.util.simpletimetracker.feature_suggestions.R +import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionSpecialViewData +import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionsButtonViewData +import com.example.util.simpletimetracker.feature_suggestions.interactor.ActivitySuggestionsCalculateInteractor +import com.example.util.simpletimetracker.feature_suggestions.interactor.ActivitySuggestionsViewDataInteractor +import com.example.util.simpletimetracker.feature_suggestions.model.ActivitySuggestionModel +import com.example.util.simpletimetracker.navigation.Router +import com.example.util.simpletimetracker.navigation.params.screen.TypesSelectionDialogParams +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ActivitySuggestionsViewModel @Inject constructor( + private val router: Router, + private val resourceRepo: ResourceRepo, + private val activitySuggestionsViewDataInteractor: ActivitySuggestionsViewDataInteractor, + private val activitySuggestionsCalculateInteractor: ActivitySuggestionsCalculateInteractor, +) : BaseViewModel() { + + val viewData: LiveData> by lazySuspend { + listOf(LoaderViewData()).also { updateViewData() } + } + + private var suggestions: List = emptyList() + private var selectingSuggestionsForTypeId: Long = 0L + + fun onTypesSelected(typeIds: List, tag: String?) = viewModelScope.launch { + when (tag) { + ACTIVITY_SUGGESTIONS_TYPE_SELECTION_TAG -> { + suggestions = typeIds.map { typeId -> + ActivitySuggestionModel( + typeId = typeId, + suggestions = suggestions.firstOrNull { + it.typeId == typeId + }?.suggestions.orEmpty(), + ) + } + updateViewData() + } + ACTIVITY_SUGGESTIONS_SUGGESTION_SELECTION_TAG -> { + suggestions = suggestions.map { suggestion -> + val newSuggestions = if ( + suggestion.typeId == selectingSuggestionsForTypeId + ) { + typeIds + } else { + suggestion.suggestions + } + ActivitySuggestionModel( + typeId = suggestion.typeId, + suggestions = newSuggestions, + ) + } + updateViewData() + } + } + } + + fun onSpecialSuggestionClick(item: ActivitySuggestionSpecialViewData) { + when (item.id.type) { + is ActivitySuggestionSpecialViewData.Type.Add -> { + val forTypeId = item.id.forTypeId + selectingSuggestionsForTypeId = forTypeId + TypesSelectionDialogParams( + tag = ACTIVITY_SUGGESTIONS_SUGGESTION_SELECTION_TAG, + title = resourceRepo.getString(R.string.change_record_message_choose_type), + subtitle = "", // TODO SUG add hint + type = TypesSelectionDialogParams.Type.Activity, + selectedTypeIds = suggestions.firstOrNull { + it.typeId == forTypeId + }?.suggestions.orEmpty(), + isMultiSelectAvailable = true, + idsShouldBeVisible = emptyList(), + showHints = true, + ).let(router::navigate) + } + is ActivitySuggestionSpecialViewData.Type.Calculate -> viewModelScope.launch { + val forTypeId = item.id.forTypeId + selectingSuggestionsForTypeId = forTypeId + val newData = activitySuggestionsCalculateInteractor + .execute(listOf(forTypeId)) + .firstOrNull { it.typeId == forTypeId } + ?.suggestions + .orEmpty() + // TODO SUG do better + onTypesSelected(newData, ACTIVITY_SUGGESTIONS_SUGGESTION_SELECTION_TAG) + } + } + } + + fun onItemButtonClick(viewData: ActivitySuggestionsButtonViewData) { + when (viewData.block) { + ActivitySuggestionsButtonViewData.Block.ADD -> { + TypesSelectionDialogParams( + tag = ACTIVITY_SUGGESTIONS_TYPE_SELECTION_TAG, + title = resourceRepo.getString(R.string.change_record_message_choose_type), + subtitle = "Suggestions will be shown for selected activities", // TODO SUG + type = TypesSelectionDialogParams.Type.Activity, + selectedTypeIds = suggestions.map { it.typeId }, + isMultiSelectAvailable = true, + idsShouldBeVisible = emptyList(), + showHints = true, + ).let(router::navigate) + } + ActivitySuggestionsButtonViewData.Block.CALCULATE -> viewModelScope.launch { + val selectedTypeIds = suggestions.map { it.typeId } + suggestions = activitySuggestionsCalculateInteractor.execute(selectedTypeIds) + updateViewData() + } + } + } + + fun onSaveClick() { + viewModelScope.launch { + // TODO SUG + router.back() + } + } + + private fun updateViewData() = viewModelScope.launch { + val data = loadViewData() + viewData.set(data) + } + + private suspend fun loadViewData(): List { + return activitySuggestionsViewDataInteractor.getViewData( + suggestions = suggestions, + ) + } + + companion object { + private const val ACTIVITY_SUGGESTIONS_TYPE_SELECTION_TAG = + "ACTIVITY_SUGGESTIONS_TYPE_SELECTION_TAG" + private const val ACTIVITY_SUGGESTIONS_SUGGESTION_SELECTION_TAG = + "ACTIVITY_SUGGESTIONS_SUGGESTION_SELECTION_TAG" + } +} diff --git a/features/feature_suggestions/src/main/res/layout/activity_suggestions_fragment.xml b/features/feature_suggestions/src/main/res/layout/activity_suggestions_fragment.xml new file mode 100644 index 000000000..7912b7b2f --- /dev/null +++ b/features/feature_suggestions/src/main/res/layout/activity_suggestions_fragment.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + diff --git a/features/feature_suggestions/src/main/res/layout/item_activity_suggestion_layout.xml b/features/feature_suggestions/src/main/res/layout/item_activity_suggestion_layout.xml new file mode 100644 index 000000000..4bb8caa71 --- /dev/null +++ b/features/feature_suggestions/src/main/res/layout/item_activity_suggestion_layout.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/features/feature_suggestions/src/main/res/layout/item_activity_suggestions_button.xml b/features/feature_suggestions/src/main/res/layout/item_activity_suggestions_button.xml new file mode 100644 index 000000000..132581b69 --- /dev/null +++ b/features/feature_suggestions/src/main/res/layout/item_activity_suggestions_button.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + diff --git a/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/ActivitySuggestionsParams.kt b/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/ActivitySuggestionsParams.kt new file mode 100644 index 000000000..808116ca4 --- /dev/null +++ b/navigation/src/main/java/com/example/util/simpletimetracker/navigation/params/screen/ActivitySuggestionsParams.kt @@ -0,0 +1,3 @@ +package com.example.util.simpletimetracker.navigation.params.screen + +object ActivitySuggestionsParams : ScreenParams \ No newline at end of file diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index d82f9c655..f0dae4016 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -188,6 +188,9 @@ Choose action Choose conditions + + Shows suggestions for the next activity based on current or last activity + Activity filters Filter removed @@ -326,6 +329,7 @@ Archive Data edit Complex rules + Activity suggestions Edit categories and tags Assign categories to activities and tags to records. Show record tag selection