From fa914845f4b6b5347bce5803f5a31d4b6bc7a022 Mon Sep 17 00:00:00 2001 From: JustinZeus <33318938+JustinZeus@users.noreply.github.com> Date: Thu, 21 May 2026 21:31:17 +0200 Subject: [PATCH] Add option to hide the voice search button (#1423) ## Description Adds a "Show voice search button" toggle to the search page's view options dialog, alongside the existing "Combined search results" toggle. When disabled, the voice search button is hidden from the search row, taking it out of the D-pad focus path. The new field `show_voice_search_button` is added to `SearchPreferences` (default `true`). The serializer applies the default for new installs, and a new entry in `AppUpgradeHandler` sets it to `true` for users upgrading from earlier versions so existing behaviour is preserved. ### Related issues Discussion: https://github.com/damontecres/Wholphin/discussions/1419 ### Testing Tested on an Android TV (1080p) AVD running API 34: - Toggle on (default): voice search button visible at the top of the search row. - Toggle off: button disappears; only the search text field remains. - Toggle back on: button reappears. - Upgrade handler verified by spoofing `version.current.name` to an older value in SharedPreferences and force-restarting the app; after `needUpgrade()` fired, the toggle was on as expected. The upgrade-handler entry currently uses `0.6.4-8-g0` (based on current `main` HEAD); happy to bump this if you'd prefer a different target version on merge. ## Screenshots pr1-01-home pr1-02-toggled-off ## AI or LLM usage I used Claude to scaffold the proto/serializer/upgrade-handler/dialog changes, mirroring the existing `combined_search_results` pattern. I reviewed each diff before committing and tested the toggle manually on the emulator, including the upgrade-handler simulation. --------- Co-authored-by: Damontecres --- .../preferences/AppPreferencesSerializer.kt | 1 + .../wholphin/services/AppUpgradeHandler.kt | 7 ++ .../wholphin/ui/main/SearchPage.kt | 87 +++++++++++++++---- app/src/main/proto/WholphinDataStore.proto | 1 + app/src/main/res/values/strings.xml | 3 + 5 files changed, 81 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index 47dae5ec..99145371 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -105,6 +105,7 @@ class AppPreferencesSerializer .newBuilder() .apply { combinedSearchResults = false + showVoiceSearchButton = true }.build() subtitlesPreferences = diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 778f300c..7ba190de 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -21,6 +21,7 @@ import com.github.damontecres.wholphin.preferences.updatePhotoPreferences import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences +import com.github.damontecres.wholphin.preferences.updateSearchPreferences import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings @@ -334,5 +335,11 @@ class AppUpgradeHandler it.updatePlaybackPreferences { cinemaMode = AppPreference.CinemaMode.defaultValue } } } + + if (previous.isEqualOrBefore(Version.fromString("0.6.4-8-g0"))) { + appPreferences.updateData { + it.updateSearchPreferences { showVoiceSearchButton = true } + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index 28d469e9..0a57b7ba 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -4,7 +4,11 @@ import android.view.Gravity import androidx.activity.compose.BackHandler import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup @@ -72,6 +76,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.updateSearchPreferences import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.SeerrService +import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus @@ -104,11 +109,7 @@ import com.github.damontecres.wholphin.util.SearchRelevance import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient @@ -127,17 +128,13 @@ class SearchViewModel private val appPreferences: DataStore, private val seerrService: SeerrService, val voiceInputManager: VoiceInputManager, + val userPreferencesService: UserPreferencesService, ) : ViewModel() { val voiceState = voiceInputManager.state val soundLevel = voiceInputManager.soundLevel val partialResult = voiceInputManager.partialResult val seerrActive = seerrService.active - val combinedModeFlow: StateFlow = - appPreferences.data - .map { it.interfacePreferences.searchPreferences.combinedSearchResults } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) - val movies = MutableLiveData(SearchResult.NoQuery) val series = MutableLiveData(SearchResult.NoQuery) val episodes = MutableLiveData(SearchResult.NoQuery) @@ -275,6 +272,16 @@ class SearchViewModel } } + fun setVoiceSearchButtonVisible(visible: Boolean) { + viewModelScope.launchIO { + appPreferences.updateData { + it.updateSearchPreferences { + showVoiceSearchButton = visible + } + } + } + } + private fun searchSeerr(query: String) { viewModelScope.launchIO { if (seerrService.active.first()) { @@ -350,7 +357,14 @@ fun SearchPage( val songs by viewModel.songs.observeAsState(SearchResult.NoQuery) val seerrResults by viewModel.seerrResults.observeAsState(SearchResult.NoQuery) val combinedResults by viewModel.combinedResults.observeAsState(SearchResult.NoQuery) - val combinedMode by viewModel.combinedModeFlow.collectAsState() + + // Start with current preferences, but collect updates when view options change + val prefs = + viewModel.userPreferencesService.flow + .collectAsState(userPreferences) + .value.appPreferences.interfacePreferences.searchPreferences + val combinedMode = prefs.combinedSearchResults + val voiceSearchButtonVisible = prefs.showVoiceSearchButton // val query = rememberTextFieldState() var query by rememberSaveable { mutableStateOf("") } @@ -499,7 +513,7 @@ fun SearchPage( } Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(0.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -508,13 +522,20 @@ fun SearchPage( .focusRestorer(textFieldFocusRequester) .focusRequester(focusRequesters[SEARCH_ROW]), ) { - VoiceSearchButton( - onSpeechResult = { spokenText -> - query = spokenText - triggerImmediateSearch(spokenText) - }, - voiceInputManager = viewModel.voiceInputManager, - ) + AnimatedVisibility( + visible = voiceSearchButtonVisible, + enter = fadeIn() + expandHorizontally(expandFrom = Alignment.End), + exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.End), + ) { + VoiceSearchButton( + onSpeechResult = { spokenText -> + query = spokenText + triggerImmediateSearch(spokenText) + }, + voiceInputManager = viewModel.voiceInputManager, + modifier = Modifier.padding(end = 12.dp), + ) + } SearchEditTextBox( value = query, @@ -547,6 +568,7 @@ fun SearchPage( title = R.string.view_options, iconStringRes = R.string.fa_sliders, onClick = { showViewOptions = true }, + modifier = Modifier.padding(start = 12.dp), ) } } @@ -757,6 +779,8 @@ fun SearchPage( SearchViewOptionsDialog( combinedResults = combinedMode, onCombinedResultsChange = viewModel::setCombinedResults, + voiceSearchButtonVisible = voiceSearchButtonVisible, + onVoiceSearchButtonVisibleChange = viewModel::setVoiceSearchButtonVisible, onDismissRequest = { showViewOptions = false }, ) } @@ -766,6 +790,8 @@ fun SearchPage( fun SearchViewOptionsDialog( combinedResults: Boolean, onCombinedResultsChange: (Boolean) -> Unit, + voiceSearchButtonVisible: Boolean, + onVoiceSearchButtonVisibleChange: (Boolean) -> Unit, onDismissRequest: () -> Unit, ) { Dialog( @@ -815,6 +841,31 @@ fun SearchViewOptionsDialog( onClick = { onCombinedResultsChange(!combinedResults) }, modifier = Modifier.fillMaxWidth(), ) + + ListItem( + selected = false, + headlineContent = { + Text(stringResource(R.string.show_voice_search_button)) + }, + supportingContent = { + Text( + if (voiceSearchButtonVisible) { + stringResource(R.string.visible_ui) + } else { + stringResource(R.string.hidden_ui) + }, + ) + }, + trailingContent = { + Switch( + checked = voiceSearchButtonVisible, + onCheckedChange = onVoiceSearchButtonVisibleChange, + colors = SwitchColors(), + ) + }, + onClick = { onVoiceSearchButtonVisibleChange(!voiceSearchButtonVisible) }, + modifier = Modifier.fillMaxWidth(), + ) } } } diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index fb72875b..f4ca3f98 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -168,6 +168,7 @@ message ScreensaverPreferences{ message SearchPreferences { bool combined_search_results = 1; + bool show_voice_search_button = 2; } enum DisplayToggle{ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ea099a7..4d5e4d22 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -449,6 +449,9 @@ Combined search results Show all results in a single list Show results by category + Show voice search button + Visible + Hidden Results Resolution switching Local