From 16fb5487eee208738ce1272b1a7aa2218b6b0104 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Jul 2026 10:19:22 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../state/FeatureTogglesScreenUM.kt | 3 + .../featuretoggles/ui/FeatureTogglesScreen.kt | 160 ++++++++++++------ .../viewmodels/FeatureTogglesViewModel.kt | 43 ++++- .../impl/src/main/res/values/strings.xml | 1 + 4 files changed, 153 insertions(+), 54 deletions(-) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesScreenUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesScreenUM.kt index fe9d1551ae..0ddaf6bc54 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesScreenUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesScreenUM.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation.featuretoggles.state +import com.tangem.core.ui.ds2.search.TangemSearch import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM import kotlinx.collections.immutable.ImmutableList @@ -8,6 +9,7 @@ import kotlinx.collections.immutable.ImmutableList * * @property topBar top bar state * @property appVersion app version + * @property searchBar search bar state used to filter [featureToggleGroups] by toggle name * @property featureToggleGroups feature toggles grouped by release status * @property onToggleValueChange the lambda to be invoked when switch button is pressed * @property onRestartAppClick the lambda to be invoked when restart app button is pressed @@ -15,6 +17,7 @@ import kotlinx.collections.immutable.ImmutableList internal data class FeatureTogglesScreenUM( val topBar: TopBarWithRefreshUM, val appVersion: String, + val searchBar: TangemSearch.State, val featureToggleGroups: ImmutableList, val onToggleValueChange: (String, Boolean) -> Unit, val onRestartAppClick: () -> Unit, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index 1d7d298c47..678840e6c4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -17,7 +18,9 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.divider.DividerWithPadding +import com.tangem.core.ui.ds2.search.TangemSearch import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme @@ -34,6 +37,8 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +private val RESTART_BUTTON_OVERLAY_HEIGHT = 80.dp + /** * Screen with feature toggles list * @@ -42,69 +47,117 @@ import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalFoundationApi::class) @Composable internal fun FeatureTogglesScreen(state: FeatureTogglesScreenUM) { - LazyColumn( + Box( modifier = Modifier .fillMaxSize() .background(TangemTheme.colors.background.secondary), ) { - stickyHeader { TopBarWithRefresh(state = state.topBar) } - - if (state.topBar.refreshButton.isVisible) { - item(key = "warning_notification", contentType = "warning_notification") { - CustomSetupNotification( - subtitle = resourceReference( - id = R.string.feature_toggles_custom_setup_warning_description, - wrappedList(state.appVersion), - ), - modifier = Modifier - .animateItem() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) - } - } else { - item(key = "warning_notification", contentType = "warning_notification") { - InitialSetupNotification( - subtitle = resourceReference( - id = R.string.feature_toggles_initial_setup_warning_description, - wrappedList(state.appVersion), - ), - modifier = Modifier - .animateItem() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) - } - } - - state.featureToggleGroups.fastForEachIndexed { index, group -> - if (index != 0) { - item(key = "divider_${group.status.name}", contentType = "group_divider") { - DividerWithPadding(horizontal = 16.dp, vertical = 8.dp) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = RESTART_BUTTON_OVERLAY_HEIGHT), + ) { + stickyHeader { + Column(modifier = Modifier.background(TangemTheme.colors.background.secondary)) { + TopBarWithRefresh(state = state.topBar) + TangemSearch( + state = state.searchBar, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + ) } } - item(key = "header_${group.status.name}", contentType = "group_header") { - GroupHeader(title = "${group.status.emoji} ${group.status.title}") - } + customSetup(state.topBar, state.appVersion) - items( - items = group.toggles, - key = TesterFeatureToggleUM::name, - contentType = { "feature_toggle_item" }, - ) { featureToggle -> - FeatureToggleItem( - toggle = featureToggle, - onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) }, - ) - } + emptySearch(state.featureToggleGroups, state.searchBar) + + featureTogglesList(state.featureToggleGroups, state.onToggleValueChange) } - item { - PrimaryButton( - text = stringResourceSafe(id = R.string.restart_app), - onClick = state.onRestartAppClick, + RestartButton( + onRestartAppClick = state.onRestartAppClick, + modifier = Modifier.align(Alignment.BottomCenter), + ) + } +} + +private fun LazyListScope.customSetup(topBar: TopBarWithRefreshUM, appVersion: String) { + if (topBar.refreshButton.isVisible) { + item(key = "warning_notification", contentType = "warning_notification") { + CustomSetupNotification( + subtitle = resourceReference( + id = R.string.feature_toggles_custom_setup_warning_description, + wrappedList(appVersion), + ), + modifier = Modifier + .animateItem() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + } else { + item(key = "warning_notification", contentType = "warning_notification") { + InitialSetupNotification( + subtitle = resourceReference( + id = R.string.feature_toggles_initial_setup_warning_description, + wrappedList(appVersion), + ), + modifier = Modifier + .animateItem() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + } +} + +private fun LazyListScope.emptySearch(featureToggleGroups: List, searchBar: TangemSearch.State) { + if (featureToggleGroups.isEmpty() && searchBar.query.isNotBlank()) { + item(key = "nothing_found", contentType = "nothing_found") { + Text( + text = stringResourceSafe(R.string.feature_toggles_no_feature_toggles_found), modifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing16), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + ) + } + } +} + +@Composable +private fun RestartButton(onRestartAppClick: () -> Unit, modifier: Modifier = Modifier) { + PrimaryButton( + text = stringResourceSafe(id = R.string.restart_app), + onClick = onRestartAppClick, + modifier = modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.secondary) + .padding(TangemTheme.dimens.spacing16), + ) +} + +private fun LazyListScope.featureTogglesList( + featureToggleGroups: List, + onToggleValueChange: (String, Boolean) -> Unit, +) { + featureToggleGroups.fastForEachIndexed { index, group -> + if (index != 0) { + item(key = "divider_${group.status.name}", contentType = "group_divider") { + DividerWithPadding(horizontal = 16.dp, vertical = 8.dp) + } + } + + item(key = "header_${group.status.name}", contentType = "group_header") { + GroupHeader(title = "${group.status.emoji} ${group.status.title}") + } + + items( + items = group.toggles, + key = TesterFeatureToggleUM::name, + contentType = { "feature_toggle_item" }, + ) { featureToggle -> + FeatureToggleItem( + toggle = featureToggle, + onCheckedChange = { isChange -> onToggleValueChange(featureToggle.name, isChange) }, ) } } @@ -189,6 +242,13 @@ private fun PreviewFeatureTogglesScreen() { ), ), appVersion = "5.38", + searchBar = TangemSearch.State( + placeholderText = stringReference(value = "Search"), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), featureToggleGroups = previewFeatureToggleGroups(), onToggleValueChange = { _, _ -> isCustomSetup = true }, onRestartAppClick = {}, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index cc090e4f0e..29f7f2c012 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -8,6 +8,8 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager import com.tangem.core.configtoggle.version.Version import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.core.ui.ds2.search.TangemSearch +import com.tangem.core.ui.extensions.resourceReference import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureToggleGroupUM @@ -65,12 +67,42 @@ internal class FeatureTogglesViewModel @Inject constructor( return FeatureTogglesScreenUM( topBar = getConfigSetupState(isPrimarySetup = true), appVersion = appInfoProvider.appVersion, - featureToggleGroups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(), + searchBar = initSearchBar(), + featureToggleGroups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(query = ""), onToggleValueChange = ::onToggleValueChange, onRestartAppClick = {}, ) } + private fun initSearchBar(): TangemSearch.State { + return TangemSearch.State( + placeholderText = resourceReference(com.tangem.core.ui.R.string.common_search), + query = "", + onQueryChange = ::onQueryChange, + isActive = false, + onActiveChange = ::onActiveChange, + onClearClick = ::onClearQuery, + ) + } + + private fun onQueryChange(query: String) { + val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(query = query) + state.update { screenUM -> + screenUM.copy( + searchBar = screenUM.searchBar.copy(query = query), + featureToggleGroups = groups, + ) + } + } + + private fun onActiveChange(isActive: Boolean) { + state.update { it.copy(searchBar = it.searchBar.copy(isActive = isActive)) } + } + + private fun onClearQuery() { + onQueryChange(query = "") + } + private fun getConfigSetupState(isPrimarySetup: Boolean): TopBarWithRefreshUM { val isMatchLocalConfig = mutableFeatureTogglesManager.isMatchLocalConfig() @@ -95,7 +127,7 @@ internal class FeatureTogglesViewModel @Inject constructor( viewModelScope.launch { mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled) - val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups() + val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(query = state.value.searchBar.query) state.update { it.copy(featureToggleGroups = groups) } // delay for smoothly update animations @@ -111,13 +143,16 @@ internal class FeatureTogglesViewModel @Inject constructor( mutableFeatureTogglesManager.recoverLocalConfig() val topBar = getConfigSetupState(isPrimarySetup = false) - val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups() + val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(query = state.value.searchBar.query) state.update { it.copy(topBar = topBar, featureToggleGroups = groups) } } } - private fun MutableFeatureTogglesManager.getTesterFeatureToggleGroups(): ImmutableList { + private fun MutableFeatureTogglesManager.getTesterFeatureToggleGroups( + query: String, + ): ImmutableList { val togglesByStatus = getFeatureToggles() + .filter { it.name.contains(query, ignoreCase = true) } .sortedWith(featureToggleComparator) .map { info -> TesterFeatureToggleUM( diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 83de0df1e4..cd08c14477 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Initial setup Toggles differs from v%s config Toggles matches v%s config + Cannot find any feature toggle by this query Providers differs from release config Restart app Environment toggles