Fixes for error handling, UI padding, & more (#1759)

## Description
Another collection of small individual fixes that don't warrant a full
PR:
- Better error handling for series and collection detail pages
- Better error handling on Discover search tab
- Fix issues when changing view options on card grid without reloading
the page
- Better error handling when changing the refresh rate or resolution
- Fix some nav drawer items disappearing briefly when restarting the app
- Fix that the discover search row would not clear when the query is
cleared

And some UI tweaks:
- Use consistent padding for the different customize home page sections
- Convert the "Continue watching click behavior" setting from a toggle
to choice dialog (the options & behaviors have not changed)

### Related issues
I think the error handling when changing the refresh rate might help
with #1737

### Testing
Emulator mostly

## Screenshots
N/A, UI changes are minor

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-07-29 10:21:49 -04:00 • committed by GitHub
parent 2463841663
commit 8bae9c7f23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 149 additions and 96 deletions

View file

@ -6,6 +6,7 @@ import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@ -391,10 +392,17 @@ class MainActivity : AppCompatActivity() {
fun changeDisplayMode(modeId: Int) {
lifecycleScope.launch(WholphinDispatchers.Main + ExceptionHandler(autoToast = true)) {
val attrs = window.attributes
if (attrs.preferredDisplayModeId != modeId) {
Timber.d("Switch preferredDisplayModeId to %s", modeId)
window.attributes = attrs.apply { preferredDisplayModeId = modeId }
try {
val attrs = window.attributes
if (attrs.preferredDisplayModeId != modeId) {
Timber.d("Switch preferredDisplayModeId to %s", modeId)
window.attributes = attrs.apply { preferredDisplayModeId = modeId }
}
} catch (ex: Exception) {
Timber.e(ex, "Error switching preferredDisplayModeId to %s", modeId)
Toast
.makeText(this@MainActivity, "Error changing display mode", Toast.LENGTH_SHORT)
.show()
}
}
}

View file

@ -218,15 +218,16 @@ sealed interface AppPreference<Pref, T> {
)
val HomeClickToPlay =
AppSwitchPreference<AppPreferences>(
AppChoicePreference<AppPreferences, Boolean>(
title = R.string.continue_watching_click_behavior,
defaultValue = false,
getter = { it.homePagePreferences.clickToPlay },
setter = { prefs, value ->
prefs.updateHomePagePreferences { clickToPlay = value }
},
summaryOn = R.string.continue_watching_click_summary_on,
summaryOff = R.string.continue_watching_click_summary_off,
displayValues = R.array.home_click_to_play_options,
indexToValue = { it != 0 },
valueToIndex = { if (it) 1 else 0 },
)
val PlayThemeMusic =

View file

@ -70,10 +70,11 @@ class NavDrawerService
userDto?.id,
discoverActive,
)
_state.update { NavDrawerItemState() }
try {
if (user != null && userDto != null && user.id == userDto.id) {
updateNavDrawer(user, userDto, discoverActive)
} else {
_state.update { NavDrawerItemState() }
}
} catch (ex: CancellationException) {
throw ex

View file

@ -253,6 +253,7 @@ class CollectionFolderViewModel
}
fun saveViewOptions(viewOptions: ViewOptions) {
position = 0
_state.update { it.copy(viewOptions = viewOptions) }
viewModelScope.launch(ExceptionHandler() + WholphinDispatchers.IO) {
saveLibraryDisplayInfo(viewOptions = viewOptions)

View file

@ -345,7 +345,7 @@ fun <T : CardGridItem> CardGrid(
items(pager.size) { index ->
val item = pager[index]
val details =
remember(index, item, cardWidthPx) {
remember(index, item, cardWidthPx, columns) {
val mod =
if ((index == currentFocusedIndex) or (currentFocusedIndex < 0 && index == 0)) {
if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index")

View file

@ -46,6 +46,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
@ -136,26 +137,33 @@ class CollectionViewModel
}
}
viewModelScope.launchDefault {
val collection =
api.userLibraryApi
.getItem(itemId)
.content
.let { BaseItem(it, false) }
backdropService.submit(collection)
val logoImageUrl =
if (ImageType.LOGO in collection.data.imageTags.orEmpty()) {
imageUrlService.getItemImageUrl(collection, ImageType.LOGO)
} else {
null
try {
val collection =
api.userLibraryApi
.getItem(itemId)
.content
.let { BaseItem(it, false) }
backdropService.submit(collection)
val logoImageUrl =
if (ImageType.LOGO in collection.data.imageTags.orEmpty()) {
imageUrlService.getItemImageUrl(collection, ImageType.LOGO)
} else {
null
}
_state.update {
it.copy(
collection = collection,
logoImageUrl = logoImageUrl,
)
}
_state.update {
it.copy(
collection = collection,
logoImageUrl = logoImageUrl,
)
listenForStateUpdates()
themeSongPlayer.playThemeFor(itemId)
} catch (ex: CancellationException) {
throw ex
} catch (ex: Exception) {
Timber.e(ex, "Error getting collection %s", itemId)
_state.update { it.copy(loadingState = LoadingState.Error(ex)) }
}
listenForStateUpdates()
themeSongPlayer.playThemeFor(itemId)
}
}
@ -179,6 +187,8 @@ class CollectionViewModel
.collectLatest { (sort, filter, separateTypes) ->
try {
updateData(sort, filter, separateTypes)
} catch (ex: CancellationException) {
throw ex
} catch (ex: Exception) {
Timber.e(
ex,

View file

@ -50,6 +50,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
@ -158,8 +159,18 @@ class SeriesViewModel
} else {
CompletableDeferred(value = EpisodeList.Loading)
}
val seasons = seasonsDeferred.await()
val episodes = episodeListDeferred.await()
val (seasons, episodes) =
try {
val seasons = seasonsDeferred.await()
val episodes = episodeListDeferred.await()
seasons to episodes
} catch (ex: CancellationException) {
throw ex
} catch (ex: Exception) {
Timber.e(ex, "Exception fetching seasons/episodes for series %s", seriesId)
_state.update { it.copy(series = DataLoadingState.Error(ex)) }
return@launchIO
}
Timber.v("Done")
if (seriesPageType == SeriesPageType.OVERVIEW && seasonEpisodeIds != null) {

View file

@ -56,6 +56,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@ -88,13 +89,20 @@ class DiscoverSearchViewModel
}
Timber.v("Starting seerr search")
seerrResults.value = SearchResult.Searching
val results =
seerrService
.search(query)
.map { seerrService.createDiscoverItem(it) }
.filter { it.type == SeerrItemType.MOVIE || it.type == SeerrItemType.TV }
Timber.v("Seerr search complete: %s results", results.size)
seerrResults.value = SearchResult.SuccessSeerr(results)
try {
val results =
seerrService
.search(query)
.map { seerrService.createDiscoverItem(it) }
.filter { it.type == SeerrItemType.MOVIE || it.type == SeerrItemType.TV }
Timber.v("Seerr search complete: %s results", results.size)
seerrResults.value = SearchResult.SuccessSeerr(results)
} catch (ex: CancellationException) {
throw ex
} catch (ex: Exception) {
Timber.e(ex, "Error during seerr search")
seerrResults.value = SearchResult.Error(ex)
}
}
}
}

View file

@ -169,21 +169,10 @@ class SearchViewModel
currentQuery = query
combinedMode = combined
if (query.isNotNullOrBlank()) {
_state.update { SearchState.searchingState }
if (combined) {
_state.update { it.copy(combinedResults = SearchResult.Searching) }
searchCombined(query)
} else {
_state.update {
it.copy(
movies = SearchResult.Searching,
series = SearchResult.Searching,
episodes = SearchResult.Searching,
collections = SearchResult.Searching,
albums = SearchResult.Searching,
artists = SearchResult.Searching,
songs = SearchResult.Searching,
)
}
searchInternal(
query,
BaseItemKind.MOVIE,
@ -219,18 +208,7 @@ class SearchViewModel
}
searchSeerr(query)
} else {
_state.update {
it.copy(
combinedResults = SearchResult.NoQuery,
movies = SearchResult.NoQuery,
series = SearchResult.NoQuery,
episodes = SearchResult.NoQuery,
collections = SearchResult.NoQuery,
albums = SearchResult.NoQuery,
artists = SearchResult.NoQuery,
songs = SearchResult.NoQuery,
)
}
_state.update { SearchState() }
}
}
@ -479,7 +457,22 @@ data class SearchState(
val songs: SearchResult = SearchResult.NoQuery,
val seerrResults: SearchResult = SearchResult.NoQuery,
val combinedResults: SearchResult = SearchResult.NoQuery,
)
) {
companion object {
val searchingState =
SearchState(
movies = SearchResult.Searching,
series = SearchResult.Searching,
episodes = SearchResult.Searching,
collections = SearchResult.Searching,
albums = SearchResult.Searching,
artists = SearchResult.Searching,
songs = SearchResult.Searching,
seerrResults = SearchResult.Searching,
combinedResults = SearchResult.Searching,
)
}
}
private const val SEARCH_ROW = 0
private const val TAB_ROW = SEARCH_ROW + 1

View file

@ -1,12 +1,9 @@
package com.github.damontecres.wholphin.ui.main.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -16,7 +13,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ListItem
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
@ -36,9 +32,7 @@ fun HomeLibraryRowTypeList(
LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
Column(modifier = modifier) {
TitleText(stringResource(R.string.add_row_for, library.name))
LazyColumn(
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
HomeSettingsLazyColumn(
modifier =
modifier
.fillMaxHeight()

View file

@ -1,10 +1,8 @@
package com.github.damontecres.wholphin.ui.main.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -249,9 +247,8 @@ fun HomeRowPresetsContent(
LaunchedEffect(Unit) { focusRequesters[0].tryRequestFocus() }
Column(modifier = modifier) {
TitleText(stringResource(R.string.display_presets))
LazyColumn(
HomeSettingsLazyColumn(
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
modifier
.fillMaxHeight()

View file

@ -69,11 +69,11 @@ fun HomeRowSettings(
LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
Column(modifier = modifier) {
TitleText(title)
LazyColumn {
HomeSettingsLazyColumn {
preferenceOptions.forEachIndexed { groupIndex, prefGroup ->
if (preferenceOptions.size > 1) {
item {
TitleText(stringResource(prefGroup.title))
SubTitleText(stringResource(prefGroup.title))
}
}
itemsIndexed(prefGroup.preferences) { index, pref ->

View file

@ -1,11 +1,8 @@
package com.github.damontecres.wholphin.ui.main.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
@ -15,7 +12,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.ifElse
@ -31,9 +27,7 @@ fun HomeSettingsAddRow(
// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
Column(modifier = modifier) {
TitleText(stringResource(R.string.add_row))
LazyColumn(
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
HomeSettingsLazyColumn(
modifier =
modifier
.fillMaxHeight()

View file

@ -1,10 +1,7 @@
package com.github.damontecres.wholphin.ui.main.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -14,7 +11,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -31,9 +27,7 @@ fun HomeSettingsFavoriteList(
TitleText(
stringResource(R.string.add_row_for, stringResource(R.string.favorites)),
)
LazyColumn(
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
HomeSettingsLazyColumn(
modifier =
modifier
.fillMaxHeight()

View file

@ -1,10 +1,8 @@
package com.github.damontecres.wholphin.ui.main.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
@ -18,7 +16,6 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
@ -51,8 +48,7 @@ fun HomeSettingsGlobal(
modifier = Modifier.fillMaxWidth(),
)
HorizontalDivider()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
HomeSettingsLazyColumn(
modifier =
modifier
.fillMaxHeight()

View file

@ -1,7 +1,13 @@
package com.github.damontecres.wholphin.ui.main.settings
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.ui.Modifier
@ -57,3 +63,21 @@ fun HomeSettingsListItem(
glow = glow,
interactionSource = interactionSource,
)
@Composable
@NonRestartableComposable
fun HomeSettingsLazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(8.dp),
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(0.dp),
content: LazyListScope.() -> Unit,
) {
LazyColumn(
modifier = modifier,
state = state,
contentPadding = contentPadding,
verticalArrangement = verticalArrangement,
content = content,
)
}

View file

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
@ -76,9 +75,8 @@ fun HomeSettingsRowList(
}
Column(modifier = modifier) {
TitleText(stringResource(R.string.customize_home))
LazyColumn(
HomeSettingsLazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
modifier
.fillMaxHeight()
@ -255,6 +253,24 @@ fun HomeRowConfigContent(
fun TitleText(
title: String,
modifier: Modifier = Modifier,
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start,
modifier =
modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 4.dp),
)
}
@Composable
@NonRestartableComposable
fun SubTitleText(
title: String,
modifier: Modifier = Modifier,
) {
Text(
text = title,

View file

@ -888,6 +888,11 @@
<item>@string/subtitle_mode_none</item>
</string-array>
<string-array name="home_click_to_play_options">
<item>@string/continue_watching_click_summary_off</item>
<item>@string/continue_watching_click_summary_on</item>
</string-array>
<string name="preferred_subtitle_language">Preferred subtitle language</string>
<string name="preferred_audio_language">Preferred audio language</string>
<string name="override_user_profile_settings">More user profile settings</string>