mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
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 <img width="1920" height="1080" alt="pr1-01-home" src="https://github.com/user-attachments/assets/0d5cdad0-5547-44f3-b4a5-5d15f71dddc0" /> <img width="1920" height="1080" alt="pr1-02-toggled-off" src="https://github.com/user-attachments/assets/1e574229-0838-487d-b943-83d394a8193d" /> ## 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 <damontecres@gmail.com>
This commit is contained in:
parent
3880e562f6
commit
fa914845f4
5 changed files with 81 additions and 18 deletions
|
|
@ -105,6 +105,7 @@ class AppPreferencesSerializer
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.apply {
|
.apply {
|
||||||
combinedSearchResults = false
|
combinedSearchResults = false
|
||||||
|
showVoiceSearchButton = true
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
subtitlesPreferences =
|
subtitlesPreferences =
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import android.view.Gravity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandHorizontally
|
||||||
import androidx.compose.animation.expandVertically
|
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.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusGroup
|
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.preferences.updateSearchPreferences
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
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.AspectRatios
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
|
@ -127,17 +128,13 @@ class SearchViewModel
|
||||||
private val appPreferences: DataStore<AppPreferences>,
|
private val appPreferences: DataStore<AppPreferences>,
|
||||||
private val seerrService: SeerrService,
|
private val seerrService: SeerrService,
|
||||||
val voiceInputManager: VoiceInputManager,
|
val voiceInputManager: VoiceInputManager,
|
||||||
|
val userPreferencesService: UserPreferencesService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val voiceState = voiceInputManager.state
|
val voiceState = voiceInputManager.state
|
||||||
val soundLevel = voiceInputManager.soundLevel
|
val soundLevel = voiceInputManager.soundLevel
|
||||||
val partialResult = voiceInputManager.partialResult
|
val partialResult = voiceInputManager.partialResult
|
||||||
val seerrActive = seerrService.active
|
val seerrActive = seerrService.active
|
||||||
|
|
||||||
val combinedModeFlow: StateFlow<Boolean> =
|
|
||||||
appPreferences.data
|
|
||||||
.map { it.interfacePreferences.searchPreferences.combinedSearchResults }
|
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
|
||||||
|
|
||||||
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 +272,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()) {
|
||||||
|
|
@ -350,7 +357,14 @@ fun SearchPage(
|
||||||
val songs by viewModel.songs.observeAsState(SearchResult.NoQuery)
|
val songs by viewModel.songs.observeAsState(SearchResult.NoQuery)
|
||||||
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()
|
|
||||||
|
// 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()
|
// val query = rememberTextFieldState()
|
||||||
var query by rememberSaveable { mutableStateOf("") }
|
var query by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
@ -499,7 +513,7 @@ fun SearchPage(
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -507,6 +521,11 @@ fun SearchPage(
|
||||||
.focusGroup()
|
.focusGroup()
|
||||||
.focusRestorer(textFieldFocusRequester)
|
.focusRestorer(textFieldFocusRequester)
|
||||||
.focusRequester(focusRequesters[SEARCH_ROW]),
|
.focusRequester(focusRequesters[SEARCH_ROW]),
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = voiceSearchButtonVisible,
|
||||||
|
enter = fadeIn() + expandHorizontally(expandFrom = Alignment.End),
|
||||||
|
exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.End),
|
||||||
) {
|
) {
|
||||||
VoiceSearchButton(
|
VoiceSearchButton(
|
||||||
onSpeechResult = { spokenText ->
|
onSpeechResult = { spokenText ->
|
||||||
|
|
@ -514,7 +533,9 @@ fun SearchPage(
|
||||||
triggerImmediateSearch(spokenText)
|
triggerImmediateSearch(spokenText)
|
||||||
},
|
},
|
||||||
voiceInputManager = viewModel.voiceInputManager,
|
voiceInputManager = viewModel.voiceInputManager,
|
||||||
|
modifier = Modifier.padding(end = 12.dp),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
SearchEditTextBox(
|
SearchEditTextBox(
|
||||||
value = query,
|
value = query,
|
||||||
|
|
@ -547,6 +568,7 @@ fun SearchPage(
|
||||||
title = R.string.view_options,
|
title = R.string.view_options,
|
||||||
iconStringRes = R.string.fa_sliders,
|
iconStringRes = R.string.fa_sliders,
|
||||||
onClick = { showViewOptions = true },
|
onClick = { showViewOptions = true },
|
||||||
|
modifier = Modifier.padding(start = 12.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -757,6 +779,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 +790,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 +841,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.visible_ui)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.hidden_ui)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = voiceSearchButtonVisible,
|
||||||
|
onCheckedChange = onVoiceSearchButtonVisibleChange,
|
||||||
|
colors = SwitchColors(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { onVoiceSearchButtonVisibleChange(!voiceSearchButtonVisible) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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="visible_ui">Visible</string>
|
||||||
|
<string name="hidden_ui">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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue