@@ -12,28 +12,31 @@ import com.redmadrobot.konfeature.FeatureValueSpec
1212import com.redmadrobot.konfeature.Konfeature
1313import com.redmadrobot.konfeature.source.FeatureValueSource
1414import kotlinx.coroutines.Dispatchers
15+ import kotlinx.coroutines.FlowPreview
1516import kotlinx.coroutines.flow.Flow
1617import kotlinx.coroutines.flow.MutableStateFlow
1718import kotlinx.coroutines.flow.asStateFlow
19+ import kotlinx.coroutines.flow.debounce
1820import kotlinx.coroutines.flow.launchIn
1921import kotlinx.coroutines.flow.onEach
2022import kotlinx.coroutines.flow.update
2123import kotlinx.coroutines.launch
2224import kotlinx.coroutines.withContext
2325
26+ private const val SEARCH_QUERY_DELAY_MILLIS = 500L
27+
2428internal class KonfeatureViewModel (
2529 private val konfeature : Konfeature ,
2630 private val debugPanelInterceptor : KonfeatureDebugPanelInterceptor ,
2731) : PluginViewModel() {
2832 private val _state = MutableStateFlow (KonfeatureViewState ())
33+ private val _searchQueryFlow = MutableStateFlow (" " )
2934
3035 val state: Flow <KonfeatureViewState > = _state .asStateFlow()
3136
3237 init {
33- debugPanelInterceptor
34- .valuesFlow
35- .onEach { updateItems() }
36- .launchIn(viewModelScope)
38+ observeKonfeatureValues()
39+ observeSearchQuery()
3740 }
3841
3942 fun onValueChanged (key : String , value : Any ) {
@@ -70,14 +73,7 @@ internal class KonfeatureViewModel(
7073 }
7174
7275 fun onCollapseAllClick () {
73- _state .update { state ->
74- val collapsedConfigs = state.items
75- .asSequence()
76- .filterIsInstance(KonfeatureItem .Config ::class .java)
77- .map { it.name }
78- .toSet()
79- state.copy(collapsedConfigs = collapsedConfigs)
80- }
76+ _state .update { state -> state.copy(collapsedConfigs = state.configs.keys) }
8177 }
8278
8379 fun onEditClick (key : String , value : Any , isDebugSource : Boolean ) {
@@ -89,33 +85,56 @@ internal class KonfeatureViewModel(
8985 }
9086
9187 fun onSearchQueryChanged (query : String ) {
92- _state .update { state ->
93- val filteredItems = filterItems(state.items, query)
94- state.copy(searchQuery = query, filteredItems = filteredItems)
95- }
88+ _state .update { state -> state.copy(searchQuery = query) }
89+ _searchQueryFlow .update { query }
90+ }
91+
92+ private fun observeKonfeatureValues () {
93+ debugPanelInterceptor.valuesFlow
94+ .onEach { updateItems() }
95+ .launchIn(viewModelScope)
96+ }
97+
98+ @OptIn(FlowPreview ::class )
99+ private fun observeSearchQuery () {
100+ _searchQueryFlow
101+ .debounce(timeoutMillis = SEARCH_QUERY_DELAY_MILLIS )
102+ .onEach { query ->
103+ _state .update { state ->
104+ state.copy(filteredItems = filterItems(state.configs, state.values, query))
105+ }
106+ }
107+ .launchIn(viewModelScope)
96108 }
97109
98110 private suspend fun updateItems () {
99- val items = withContext(Dispatchers .IO ) { getItems(konfeature) }
111+ val (configs, values) = withContext(Dispatchers .IO ) { getItems(konfeature) }
112+ val searchQuery = _searchQueryFlow .value
113+ val filteredItems = filterItems(configs, values, searchQuery)
114+
100115 _state .update { state ->
101- val filteredItems = filterItems(items, state.searchQuery)
102- state.copy(items = items, filteredItems = filteredItems)
116+ state.copy(configs = configs, values = values, filteredItems = filteredItems)
103117 }
104118 }
105119
106- private fun getItems (konfeature : Konfeature ): List <KonfeatureItem > {
107- return konfeature.spec.fold(mutableListOf<KonfeatureItem >()) { acc, configSpec ->
120+ private fun getItems (konfeature : Konfeature ): Pair <Map <String , KonfeatureItem .Config >, List<KonfeatureItem.Value>> {
121+ val configs = mutableMapOf<String , KonfeatureItem .Config >()
122+ val values = mutableListOf<KonfeatureItem .Value >()
123+
124+ konfeature.spec.fold(configs to values) { acc, configSpec ->
108125 acc.apply {
109- add( createConfigItem(configSpec) )
110- addAll( configSpec.values.map { valueSpec ->
126+ configs[configSpec.name] = createConfigItem(configSpec)
127+ configSpec.values.mapTo(values) { valueSpec ->
111128 createConfigValueItem(
112129 configName = configSpec.name,
113130 valueSpec = valueSpec,
114131 konfeature = konfeature
115132 )
116- })
133+ }
117134 }
118135 }
136+
137+ return configs to values
119138 }
120139
121140 private fun createConfigItem (config : FeatureConfigSpec ): KonfeatureItem .Config {
@@ -162,19 +181,24 @@ internal class KonfeatureViewModel(
162181 }
163182 }
164183
165- private fun filterItems (items : List <KonfeatureItem >, query : String ): List <KonfeatureItem > {
166- if (query.isBlank()) return items
167-
168- val formattedQuery = query.lowercase()
169- val matchingItems = items
170- .filterIsInstance<KonfeatureItem .Value >()
171- .filter { it.key.lowercase().contains(formattedQuery) }
172- val matchingConfigNames = matchingItems.map { it.configName }.toSet()
173-
174- return items.filter { item ->
175- when (item) {
176- is KonfeatureItem .Config -> item.name in matchingConfigNames
177- is KonfeatureItem .Value -> item.key.lowercase().contains(formattedQuery)
184+ private suspend fun filterItems (
185+ configs : Map <String , KonfeatureItem .Config >,
186+ values : List <KonfeatureItem .Value >,
187+ query : String
188+ ): List <KonfeatureItem > {
189+ return withContext(Dispatchers .Default ) {
190+ buildList {
191+ var previousValue: KonfeatureItem .Value ? = null
192+
193+ for (value in values) {
194+ if (value.key.contains(query, ignoreCase = true )) {
195+ if (previousValue?.configName != value.configName) {
196+ configs[value.configName]?.let { config -> add(config) }
197+ }
198+ add(value)
199+ previousValue = value
200+ }
201+ }
178202 }
179203 }
180204 }
0 commit comments