diff --git a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt index 5f8fd38cd4..bbc8a3955a 100644 --- a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt +++ b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import com.ivy.legacy.IvyWalletPreview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -51,9 +52,11 @@ import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style +import com.ivy.legacy.ui.SearchInput import com.ivy.legacy.utils.balancePrefix import com.ivy.legacy.utils.compactBalancePrefix import com.ivy.legacy.utils.format +import com.ivy.legacy.utils.selectEndTextFieldValue import com.ivy.navigation.CategoriesScreen import com.ivy.navigation.TransactionsScreen import com.ivy.navigation.navigation @@ -100,7 +103,10 @@ fun BoxWithConstraintsScope.CategoriesScreen(screen: CategoriesScreen) { @Composable private fun BoxWithConstraintsScope.UI( - state: CategoriesScreenState = CategoriesScreenState(compactCategoriesModeEnabled = false), + state: CategoriesScreenState = CategoriesScreenState( + compactCategoriesModeEnabled = false, + showCategorySearchBar = false + ), onEvent: (CategoriesScreenEvent) -> Unit = {} ) { val nav = navigation() @@ -158,6 +164,10 @@ private fun BoxWithConstraintsScope.UI( Spacer(Modifier.width(24.dp)) } + if (state.showCategorySearchBar) { + Spacer(Modifier.height(16.dp)) + SearchField(onSearch = { onEvent(CategoriesScreenEvent.OnSearchQueryUpdate(it)) }) + } Spacer(Modifier.height(16.dp)) } @@ -679,11 +689,102 @@ private fun PreviewCategoriesCompactModeEnabled(theme: Theme = Theme.LIGHT) { @Preview @Composable -private fun Preview(theme: Theme = Theme.LIGHT, compactModeEnabled: Boolean = false) { - com.ivy.legacy.IvyWalletPreview(theme) { +private fun PreviewCategoriesCompactModeEnabledAndSearchBarEnabled(theme: Theme = Theme.LIGHT) { + Preview(theme = theme, compactModeEnabled = true, displaySearchBarEnabled = true) +} + +@Preview +@Composable +private fun Preview( + theme: Theme = Theme.LIGHT, + compactModeEnabled: Boolean = false, + displaySearchBarEnabled: Boolean = false +) { + IvyWalletPreview(theme) { + val state = CategoriesScreenState( + baseCurrency = "BGN", + compactCategoriesModeEnabled = compactModeEnabled, + showCategorySearchBar = displaySearchBarEnabled, + categories = persistentListOf( + CategoryData( + category = Category( + id = CategoryId(UUID.randomUUID()), + name = NotBlankTrimmedString.unsafe("Groceries"), + color = ColorInt(Green.toArgb()), + icon = IconAsset.unsafe("groceries"), + orderNum = 0.0, + ), + monthlyBalance = 2125.0, + monthlyExpenses = 920.0, + monthlyIncome = 3045.0 + ), + CategoryData( + category = Category( + id = CategoryId(UUID.randomUUID()), + name = NotBlankTrimmedString.unsafe("Fun"), + color = ColorInt(Orange.toArgb()), + icon = IconAsset.unsafe("game"), + orderNum = 0.0, + ), + monthlyBalance = 1200.0, + monthlyExpenses = 750.0, + monthlyIncome = 0.0 + ), + CategoryData( + category = Category( + id = CategoryId(UUID.randomUUID()), + name = NotBlankTrimmedString.unsafe("Ivy"), + color = ColorInt(IvyDark.toArgb()), + icon = IconAsset.unsafe("star"), + orderNum = 0.0, + ), + monthlyBalance = 1200.0, + monthlyExpenses = 0.0, + monthlyIncome = 5000.0 + ), + CategoryData( + category = Category( + id = CategoryId(UUID.randomUUID()), + name = NotBlankTrimmedString.unsafe("Food"), + color = ColorInt(GreenLight.toArgb()), + icon = IconAsset.unsafe("atom"), + orderNum = 0.0, + ), + monthlyBalance = 12125.21, + monthlyExpenses = 1350.50, + monthlyIncome = 8000.48 + ), + CategoryData( + category = Category( + id = CategoryId(UUID.randomUUID()), + name = NotBlankTrimmedString.unsafe("Shisha"), + color = ColorInt(GreenDark.toArgb()), + icon = IconAsset.unsafe("drink"), + orderNum = 0.0, + ), + monthlyBalance = 820.0, + monthlyExpenses = 340.0, + monthlyIncome = 400.0 + ), + + ) + ) + UI(state = state) + } +} + +@Preview +@Composable +private fun PreviewWithSearchBarEnabled( + theme: Theme = Theme.LIGHT, + compactModeEnabled: Boolean = false, + displaySearchBarEnabled: Boolean = true +) { + IvyWalletPreview(theme) { val state = CategoriesScreenState( baseCurrency = "BGN", compactCategoriesModeEnabled = compactModeEnabled, + showCategorySearchBar = displaySearchBarEnabled, categories = persistentListOf( CategoryData( category = Category( @@ -752,6 +853,26 @@ private fun Preview(theme: Theme = Theme.LIGHT, compactModeEnabled: Boolean = fa } } +@Composable +private fun SearchField( + onSearch: (String) -> Unit, +) { + var searchQueryTextFieldValue by remember { + mutableStateOf(selectEndTextFieldValue("")) + } + + SearchInput( + searchQueryTextFieldValue = searchQueryTextFieldValue, + hint = "Search categories", + focus = false, + showClearIcon = searchQueryTextFieldValue.text.isNotEmpty(), + onSetSearchQueryTextField = { + searchQueryTextFieldValue = it + onSearch(it.text) + } + ) +} + /** For screenshot testing */ @Composable fun CategoriesScreenUiTest(isDark: Boolean) { @@ -762,6 +883,16 @@ fun CategoriesScreenUiTest(isDark: Boolean) { Preview(theme) } +/** For screenshot testing */ +@Composable +fun CategoriesScreenWithSearchBarUiTest(isDark: Boolean) { + val theme = when (isDark) { + true -> Theme.DARK + false -> Theme.LIGHT + } + Preview(theme = theme, displaySearchBarEnabled = true) +} + /** For screenshot testing */ @Composable fun CategoriesScreenCompactUiTest(isDark: Boolean) { @@ -770,4 +901,14 @@ fun CategoriesScreenCompactUiTest(isDark: Boolean) { false -> Theme.LIGHT } Preview(theme, compactModeEnabled = true) +} + +/** For screenshot testing */ +@Composable +fun CategoriesScreenWithSearchBarCompactUiTest(isDark: Boolean) { + val theme = when (isDark) { + true -> Theme.DARK + false -> Theme.LIGHT + } + Preview(theme, compactModeEnabled = true, displaySearchBarEnabled = true) } \ No newline at end of file diff --git a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreenEvent.kt b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreenEvent.kt index b6e538c139..527995dbf4 100644 --- a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreenEvent.kt +++ b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreenEvent.kt @@ -17,4 +17,5 @@ sealed interface CategoriesScreenEvent { data class OnSortOrderModalVisible(val visible: Boolean) : CategoriesScreenEvent data class OnCategoryModalVisible(val categoryModalData: CategoryModalData?) : CategoriesScreenEvent + data class OnSearchQueryUpdate(val queryString: String) : CategoriesScreenEvent } diff --git a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreenState.kt b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreenState.kt index 4cbbaa5b46..7988b8196b 100644 --- a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreenState.kt +++ b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreenState.kt @@ -15,4 +15,6 @@ data class CategoriesScreenState( val sortOrderItems: ImmutableList = SortOrder.values().toList().toImmutableList(), val sortOrder: SortOrder = SortOrder.DEFAULT, val compactCategoriesModeEnabled: Boolean, + val showCategorySearchBar: Boolean, + ) diff --git a/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt b/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt index a92b8e3c7a..d02f707bac 100644 --- a/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt +++ b/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt @@ -4,12 +4,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.SharedPrefs import com.ivy.base.legacy.Transaction import com.ivy.base.time.TimeConverter import com.ivy.base.time.TimeProvider -import com.ivy.ui.ComposeViewModel import com.ivy.data.repository.CategoryRepository import com.ivy.domain.features.Features import com.ivy.frp.action.thenMap @@ -17,6 +17,7 @@ import com.ivy.frp.thenInvokeAfter import com.ivy.legacy.data.model.TimePeriod import com.ivy.legacy.datamodel.Account import com.ivy.legacy.utils.ioThread +import com.ivy.ui.ComposeViewModel import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.action.category.LegacyCategoryIncomeWithAccountFiltersAct import com.ivy.wallet.domain.action.settings.BaseCurrencyAct @@ -55,6 +56,7 @@ class CategoriesViewModel @Inject constructor( private val baseCurrency = mutableStateOf("") private val categories = mutableStateOf>(persistentListOf()) + private val searchQuery = mutableStateOf("") private val reorderModalVisible = mutableStateOf(false) private val categoryModalData = mutableStateOf(null) private val sortModalVisible = mutableStateOf(false) @@ -74,6 +76,7 @@ class CategoriesViewModel @Inject constructor( sortOrder = getSortOrder(), sortModalVisible = getSortModalVisible(), compactCategoriesModeEnabled = getCompactCategoriesMode(), + showCategorySearchBar = getShowCategorySearchBar() ) } @@ -82,6 +85,11 @@ class CategoriesViewModel @Inject constructor( return features.compactCategoriesMode.asEnabledState() } + @Composable + private fun getShowCategorySearchBar(): Boolean { + return features.showCategorySearchBar.asEnabledState() + } + @Composable private fun getBaseCurrency(): String { return baseCurrency.value @@ -89,7 +97,12 @@ class CategoriesViewModel @Inject constructor( @Composable private fun getCategories(): ImmutableList { - return categories.value + val allCats = categories.value + return remember(allCats, searchQuery.value) { + allCats.filter { + searchQuery.value.lowercase().trim() in it.category.name.toString().lowercase() + }.toImmutableList() + } } @Composable @@ -126,7 +139,11 @@ class CategoriesViewModel @Inject constructor( ioThread { val range = TimePeriod.currentMonth( startDayOfMonth = ivyContext.startDayOfMonth - ).toRange(ivyContext.startDayOfMonth, timeConverter, timeProvider) // this must be monthly + ).toRange( + ivyContext.startDayOfMonth, + timeConverter, + timeProvider + ) // this must be monthly allAccounts = accountsAct(Unit) baseCurrency.value = baseCurrencyAct(Unit) @@ -135,7 +152,7 @@ class CategoriesViewModel @Inject constructor( TrnsWithRangeAndAccFiltersAct.Input( range = range, accountIdFilterSet = suspend { allAccounts } thenMap { it.id } - thenInvokeAfter { it.toHashSet() } + thenInvokeAfter { it.toHashSet() } ) ) @@ -171,11 +188,14 @@ class CategoriesViewModel @Inject constructor( } val sortedList = sortList(categories, sortOrder.value).toImmutableList() - this.categories.value = sortedList } } + private fun updateSearchQuery(queryString: String) { + searchQuery.value = queryString + } + private suspend fun reorder( newOrder: List, sortOrder: SortOrder = SortOrder.DEFAULT @@ -244,6 +264,8 @@ class CategoriesViewModel @Inject constructor( is CategoriesScreenEvent.OnCategoryModalVisible -> { categoryModalData.value = event.categoryModalData } + + is CategoriesScreenEvent.OnSearchQueryUpdate -> updateSearchQuery(event.queryString) } } } diff --git a/screen/categories/src/test/java/com/ivy/categories/CategoriesScreenPaparazziTest.kt b/screen/categories/src/test/java/com/ivy/categories/CategoriesScreenPaparazziTest.kt index 1a8e1297a5..59676c227d 100644 --- a/screen/categories/src/test/java/com/ivy/categories/CategoriesScreenPaparazziTest.kt +++ b/screen/categories/src/test/java/com/ivy/categories/CategoriesScreenPaparazziTest.kt @@ -19,10 +19,24 @@ class CategoriesScreenPaparazziTest( } } + @Test + fun `snapshot Categories nonCompact Screen with search bar`() { + snapshot(theme) { + CategoriesScreenWithSearchBarUiTest(theme == PaparazziTheme.Dark) + } + } + @Test fun `snapshot Categories compact Screen`() { snapshot(theme) { CategoriesScreenCompactUiTest(theme == PaparazziTheme.Dark) } } -} \ No newline at end of file + + @Test + fun `snapshot Categories compact Screen with search bar`() { + snapshot(theme) { + CategoriesScreenWithSearchBarCompactUiTest(theme == PaparazziTheme.Dark) + } + } +} diff --git a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen with search bar[Dark].png b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen with search bar[Dark].png new file mode 100644 index 0000000000..acf263e992 Binary files /dev/null and b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen with search bar[Dark].png differ diff --git a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen with search bar[Light].png b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen with search bar[Light].png new file mode 100644 index 0000000000..9432f58ac5 Binary files /dev/null and b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen with search bar[Light].png differ diff --git a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen[Dark].png b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen[Dark].png index f6e6641af1..0b98e4ba5f 100644 Binary files a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen[Dark].png and b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen[Dark].png differ diff --git a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen[Light].png b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen[Light].png index 20c81ef509..e083429b8c 100644 Binary files a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen[Light].png and b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories compact Screen[Light].png differ diff --git a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen with search bar[Dark].png b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen with search bar[Dark].png new file mode 100644 index 0000000000..71c3cfe852 Binary files /dev/null and b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen with search bar[Dark].png differ diff --git a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen with search bar[Light].png b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen with search bar[Light].png new file mode 100644 index 0000000000..52a835ed3c Binary files /dev/null and b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen with search bar[Light].png differ diff --git a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen[Dark].png b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen[Dark].png index 38e87daa35..bfef171f80 100644 Binary files a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen[Dark].png and b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen[Dark].png differ diff --git a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen[Light].png b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen[Light].png index d2c9b42df3..507f56e4a9 100644 Binary files a/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen[Light].png and b/screen/categories/src/test/snapshots/images/com.ivy.categories_CategoriesScreenPaparazziTest_snapshot Categories nonCompact Screen[Light].png differ diff --git a/shared/domain/src/main/java/com/ivy/domain/features/Features.kt b/shared/domain/src/main/java/com/ivy/domain/features/Features.kt index 561796e3eb..fedb4c25f7 100644 --- a/shared/domain/src/main/java/com/ivy/domain/features/Features.kt +++ b/shared/domain/src/main/java/com/ivy/domain/features/Features.kt @@ -5,6 +5,7 @@ interface Features { val compactAccountsMode: BoolFeature val compactCategoriesMode: BoolFeature val showTitleSuggestions: BoolFeature + val showCategorySearchBar: BoolFeature val hideTotalBalance: BoolFeature val allFeatures: List diff --git a/shared/domain/src/main/java/com/ivy/domain/features/IvyFeatures.kt b/shared/domain/src/main/java/com/ivy/domain/features/IvyFeatures.kt index a565ce493f..a946c8a23e 100644 --- a/shared/domain/src/main/java/com/ivy/domain/features/IvyFeatures.kt +++ b/shared/domain/src/main/java/com/ivy/domain/features/IvyFeatures.kt @@ -32,6 +32,13 @@ class IvyFeatures @Inject constructor() : Features { defaultValue = true ) + override val showCategorySearchBar = BoolFeature( + key = "search_categories", + name = "Search categories", + description = "Show search bar in category screen", + defaultValue = true + ) + override val hideTotalBalance = BoolFeature( key = "hide_total_balance", name = "Hide total balance", @@ -45,6 +52,7 @@ class IvyFeatures @Inject constructor() : Features { compactAccountsMode, compactCategoriesMode, showTitleSuggestions, + showCategorySearchBar, hideTotalBalance ) } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/SearchInput.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/SearchInput.kt index fca8567fe6..6f5f3093e5 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/SearchInput.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/SearchInput.kt @@ -29,6 +29,7 @@ fun SearchInput( searchQueryTextFieldValue: TextFieldValue, hint: String, focus: Boolean = true, + showClearIcon: Boolean = true, onSetSearchQueryTextField: (TextFieldValue) -> Unit ) { Row( @@ -61,13 +62,15 @@ fun SearchInput( } } - IvyIcon( - modifier = Modifier - .weight(1f) - .clickable { - onSetSearchQueryTextField(selectEndTextFieldValue("")) - }, - icon = R.drawable.ic_outline_clear_24 - ) + if (showClearIcon) { + IvyIcon( + modifier = Modifier + .weight(1f) + .clickable { + onSetSearchQueryTextField(selectEndTextFieldValue("")) + }, + icon = R.drawable.ic_outline_clear_24 + ) + } } }