Loading app/src/main/java/foundation/e/apps/data/playstore/search/PlayStoreSuggestionSource.kt 0 → 100644 +49 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.playstore.search import foundation.e.apps.data.playstore.PlayStoreSearchHelper import foundation.e.apps.data.search.SuggestionSource import java.util.Locale import javax.inject.Inject private const val MAX_SUGGESTIONS = 10 class PlayStoreSuggestionSource @Inject constructor( private val playStoreSearchHelper: PlayStoreSearchHelper, ) : SuggestionSource { override suspend fun suggest(query: String): List<String> { val trimmed = query.trim() if (trimmed.isEmpty()) { return emptyList() } return runCatching { playStoreSearchHelper.getSearchSuggestions(trimmed) .asSequence() .map { it.suggestion } .map { it.trim() } .filter { it.isNotEmpty() } .distinctBy { it.lowercase(Locale.getDefault()) } .take(MAX_SUGGESTIONS) .toList() }.getOrElse { emptyList() } } } app/src/main/java/foundation/e/apps/data/search/SuggestionSource.kt 0 → 100644 +23 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.search interface SuggestionSource { suspend fun suggest(query: String): List<String> } app/src/main/java/foundation/e/apps/di/SearchSuggestionModule.kt 0 → 100644 +35 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.di import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.playstore.search.PlayStoreSuggestionSource import foundation.e.apps.data.search.SuggestionSource import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class SearchSuggestionModule { @Binds @Singleton abstract fun bindSuggestionSource(impl: PlayStoreSuggestionSource): SuggestionSource } app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +77 −1 Original line number Diff line number Diff line Loading @@ -19,8 +19,84 @@ package foundation.e.apps.ui.search.v2 import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.search.SuggestionSource import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject private const val SUGGESTION_DEBOUNCE_MS = 200L data class SearchUiState( val query: String = "", val suggestions: List<String> = emptyList(), val isSuggestionVisible: Boolean = false, ) @HiltViewModel class SearchViewModelV2 @Inject constructor() : ViewModel() class SearchViewModelV2 @Inject constructor( private val suggestionSource: SuggestionSource, ) : ViewModel() { private val _uiState = MutableStateFlow(SearchUiState()) val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow() private var suggestionJob: Job? = null fun onQueryChanged(newQuery: String) { _uiState.update { current -> current.copy(query = newQuery) } suggestionJob?.cancel() if (newQuery.isBlank()) { _uiState.update { current -> current.copy( suggestions = emptyList(), isSuggestionVisible = false, ) } return } suggestionJob = viewModelScope.launch { delay(SUGGESTION_DEBOUNCE_MS) val suggestions = suggestionSource.suggest(newQuery) _uiState.update { current -> current.copy( suggestions = suggestions, isSuggestionVisible = suggestions.isNotEmpty(), ) } } } fun onSuggestionSelected(suggestion: String) { _uiState.update { current -> current.copy( query = suggestion, suggestions = emptyList(), isSuggestionVisible = false, ) } } fun onQueryCleared() { suggestionJob?.cancel() _uiState.value = SearchUiState() } fun onSearchSubmitted(submitted: String) { _uiState.update { current -> current.copy( query = submitted, suggestions = emptyList(), isSuggestionVisible = false, ) } } } Loading
app/src/main/java/foundation/e/apps/data/playstore/search/PlayStoreSuggestionSource.kt 0 → 100644 +49 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.playstore.search import foundation.e.apps.data.playstore.PlayStoreSearchHelper import foundation.e.apps.data.search.SuggestionSource import java.util.Locale import javax.inject.Inject private const val MAX_SUGGESTIONS = 10 class PlayStoreSuggestionSource @Inject constructor( private val playStoreSearchHelper: PlayStoreSearchHelper, ) : SuggestionSource { override suspend fun suggest(query: String): List<String> { val trimmed = query.trim() if (trimmed.isEmpty()) { return emptyList() } return runCatching { playStoreSearchHelper.getSearchSuggestions(trimmed) .asSequence() .map { it.suggestion } .map { it.trim() } .filter { it.isNotEmpty() } .distinctBy { it.lowercase(Locale.getDefault()) } .take(MAX_SUGGESTIONS) .toList() }.getOrElse { emptyList() } } }
app/src/main/java/foundation/e/apps/data/search/SuggestionSource.kt 0 → 100644 +23 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.search interface SuggestionSource { suspend fun suggest(query: String): List<String> }
app/src/main/java/foundation/e/apps/di/SearchSuggestionModule.kt 0 → 100644 +35 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.di import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.playstore.search.PlayStoreSuggestionSource import foundation.e.apps.data.search.SuggestionSource import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class SearchSuggestionModule { @Binds @Singleton abstract fun bindSuggestionSource(impl: PlayStoreSuggestionSource): SuggestionSource }
app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +77 −1 Original line number Diff line number Diff line Loading @@ -19,8 +19,84 @@ package foundation.e.apps.ui.search.v2 import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.search.SuggestionSource import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject private const val SUGGESTION_DEBOUNCE_MS = 200L data class SearchUiState( val query: String = "", val suggestions: List<String> = emptyList(), val isSuggestionVisible: Boolean = false, ) @HiltViewModel class SearchViewModelV2 @Inject constructor() : ViewModel() class SearchViewModelV2 @Inject constructor( private val suggestionSource: SuggestionSource, ) : ViewModel() { private val _uiState = MutableStateFlow(SearchUiState()) val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow() private var suggestionJob: Job? = null fun onQueryChanged(newQuery: String) { _uiState.update { current -> current.copy(query = newQuery) } suggestionJob?.cancel() if (newQuery.isBlank()) { _uiState.update { current -> current.copy( suggestions = emptyList(), isSuggestionVisible = false, ) } return } suggestionJob = viewModelScope.launch { delay(SUGGESTION_DEBOUNCE_MS) val suggestions = suggestionSource.suggest(newQuery) _uiState.update { current -> current.copy( suggestions = suggestions, isSuggestionVisible = suggestions.isNotEmpty(), ) } } } fun onSuggestionSelected(suggestion: String) { _uiState.update { current -> current.copy( query = suggestion, suggestions = emptyList(), isSuggestionVisible = false, ) } } fun onQueryCleared() { suggestionJob?.cancel() _uiState.value = SearchUiState() } fun onSearchSubmitted(submitted: String) { _uiState.update { current -> current.copy( query = submitted, suggestions = emptyList(), isSuggestionVisible = false, ) } } }