Add option to hide the voice search button

Adds a toggle to SearchPreferences (`show_voice_search_button`,
default true) and a new entry in the search page's view options
dialog. When disabled, the voice search button is hidden from
the search row.

For existing users, an entry in AppUpgradeHandler sets the value
to true so the button stays visible after upgrade.
This commit is contained in:
Justin Visser 2026-05-21 13:01:12 +02:00
parent 366483599c
commit bfbceb1999
5 changed files with 66 additions and 7 deletions

View file

@ -105,6 +105,7 @@ class AppPreferencesSerializer
.newBuilder() .newBuilder()
.apply { .apply {
combinedSearchResults = false combinedSearchResults = false
showVoiceSearchButton = true
}.build() }.build()
subtitlesPreferences = subtitlesPreferences =

View file

@ -21,6 +21,7 @@ import com.github.damontecres.wholphin.preferences.updatePhotoPreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences 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.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
@ -334,5 +335,11 @@ class AppUpgradeHandler
it.updatePlaybackPreferences { cinemaMode = AppPreference.CinemaMode.defaultValue } it.updatePlaybackPreferences { cinemaMode = AppPreference.CinemaMode.defaultValue }
} }
} }
if (previous.isEqualOrBefore(Version.fromString("0.6.4-8-g0"))) {
appPreferences.updateData {
it.updateSearchPreferences { showVoiceSearchButton = true }
}
}
} }
} }

View file

@ -138,6 +138,11 @@ class SearchViewModel
.map { it.interfacePreferences.searchPreferences.combinedSearchResults } .map { it.interfacePreferences.searchPreferences.combinedSearchResults }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
val voiceSearchButtonVisibleFlow: StateFlow<Boolean> =
appPreferences.data
.map { it.interfacePreferences.searchPreferences.showVoiceSearchButton }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)
val movies = MutableLiveData<SearchResult>(SearchResult.NoQuery) val movies = MutableLiveData<SearchResult>(SearchResult.NoQuery)
val series = MutableLiveData<SearchResult>(SearchResult.NoQuery) val series = MutableLiveData<SearchResult>(SearchResult.NoQuery)
val episodes = MutableLiveData<SearchResult>(SearchResult.NoQuery) val episodes = MutableLiveData<SearchResult>(SearchResult.NoQuery)
@ -275,6 +280,16 @@ class SearchViewModel
} }
} }
fun setVoiceSearchButtonVisible(visible: Boolean) {
viewModelScope.launchIO {
appPreferences.updateData {
it.updateSearchPreferences {
showVoiceSearchButton = visible
}
}
}
}
private fun searchSeerr(query: String) { private fun searchSeerr(query: String) {
viewModelScope.launchIO { viewModelScope.launchIO {
if (seerrService.active.first()) { if (seerrService.active.first()) {
@ -351,6 +366,7 @@ fun SearchPage(
val seerrResults by viewModel.seerrResults.observeAsState(SearchResult.NoQuery) val seerrResults by viewModel.seerrResults.observeAsState(SearchResult.NoQuery)
val combinedResults by viewModel.combinedResults.observeAsState(SearchResult.NoQuery) val combinedResults by viewModel.combinedResults.observeAsState(SearchResult.NoQuery)
val combinedMode by viewModel.combinedModeFlow.collectAsState() val combinedMode by viewModel.combinedModeFlow.collectAsState()
val voiceSearchButtonVisible by viewModel.voiceSearchButtonVisibleFlow.collectAsState()
// val query = rememberTextFieldState() // val query = rememberTextFieldState()
var query by rememberSaveable { mutableStateOf("") } var query by rememberSaveable { mutableStateOf("") }
@ -508,13 +524,15 @@ fun SearchPage(
.focusRestorer(textFieldFocusRequester) .focusRestorer(textFieldFocusRequester)
.focusRequester(focusRequesters[SEARCH_ROW]), .focusRequester(focusRequesters[SEARCH_ROW]),
) { ) {
VoiceSearchButton( if (voiceSearchButtonVisible) {
onSpeechResult = { spokenText -> VoiceSearchButton(
query = spokenText onSpeechResult = { spokenText ->
triggerImmediateSearch(spokenText) query = spokenText
}, triggerImmediateSearch(spokenText)
voiceInputManager = viewModel.voiceInputManager, },
) voiceInputManager = viewModel.voiceInputManager,
)
}
SearchEditTextBox( SearchEditTextBox(
value = query, value = query,
@ -757,6 +775,8 @@ fun SearchPage(
SearchViewOptionsDialog( SearchViewOptionsDialog(
combinedResults = combinedMode, combinedResults = combinedMode,
onCombinedResultsChange = viewModel::setCombinedResults, onCombinedResultsChange = viewModel::setCombinedResults,
voiceSearchButtonVisible = voiceSearchButtonVisible,
onVoiceSearchButtonVisibleChange = viewModel::setVoiceSearchButtonVisible,
onDismissRequest = { showViewOptions = false }, onDismissRequest = { showViewOptions = false },
) )
} }
@ -766,6 +786,8 @@ fun SearchPage(
fun SearchViewOptionsDialog( fun SearchViewOptionsDialog(
combinedResults: Boolean, combinedResults: Boolean,
onCombinedResultsChange: (Boolean) -> Unit, onCombinedResultsChange: (Boolean) -> Unit,
voiceSearchButtonVisible: Boolean,
onVoiceSearchButtonVisibleChange: (Boolean) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
Dialog( Dialog(
@ -815,6 +837,31 @@ fun SearchViewOptionsDialog(
onClick = { onCombinedResultsChange(!combinedResults) }, onClick = { onCombinedResultsChange(!combinedResults) },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
ListItem(
selected = false,
headlineContent = {
Text(stringResource(R.string.show_voice_search_button))
},
supportingContent = {
Text(
if (voiceSearchButtonVisible) {
stringResource(R.string.show_voice_search_button_on)
} else {
stringResource(R.string.show_voice_search_button_off)
},
)
},
trailingContent = {
Switch(
checked = voiceSearchButtonVisible,
onCheckedChange = onVoiceSearchButtonVisibleChange,
colors = SwitchColors(),
)
},
onClick = { onVoiceSearchButtonVisibleChange(!voiceSearchButtonVisible) },
modifier = Modifier.fillMaxWidth(),
)
} }
} }
} }

View file

@ -168,6 +168,7 @@ message ScreensaverPreferences{
message SearchPreferences { message SearchPreferences {
bool combined_search_results = 1; bool combined_search_results = 1;
bool show_voice_search_button = 2;
} }
enum DisplayToggle{ enum DisplayToggle{

View file

@ -449,6 +449,9 @@
<string name="combined_search_results">Combined search results</string> <string name="combined_search_results">Combined search results</string>
<string name="combined_search_results_on">Show all results in a single list</string> <string name="combined_search_results_on">Show all results in a single list</string>
<string name="combined_search_results_off">Show results by category</string> <string name="combined_search_results_off">Show results by category</string>
<string name="show_voice_search_button">Show voice search button</string>
<string name="show_voice_search_button_on">Visible</string>
<string name="show_voice_search_button_off">Hidden</string>
<string name="results">Results</string> <string name="results">Results</string>
<string name="resolution_switching">Resolution switching</string> <string name="resolution_switching">Resolution switching</string>
<string name="local">Local</string> <string name="local">Local</string>