From bb19ba316b5d8eab93a2befe7e9988e0252584d1 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 19 Oct 2025 00:08:21 -0400 Subject: [PATCH] Various UI updates (#35) Fixes and tweaks several UI issues --- .../ui/components/RecommendedMovie.kt | 5 +++- .../ui/components/RecommendedTvShow.kt | 5 +++- .../ui/detail/CollectionFolderMovie.kt | 3 +++ .../wholphin/ui/detail/CollectionFolderTv.kt | 3 +++ .../wholphin/ui/detail/ItemViewModel.kt | 15 +++++++---- .../wholphin/ui/detail/SeriesDetails.kt | 27 ++++++++++++++++--- .../wholphin/ui/detail/SeriesViewModel.kt | 2 +- .../wholphin/ui/detail/movie/MovieDetails.kt | 4 +-- .../ui/detail/movie/MovieDetailsHeader.kt | 19 ++++++++++++- .../damontecres/wholphin/ui/main/HomePage.kt | 13 ++++++++- 10 files changed, 80 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt index d9969914..df842cf3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt @@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomeRow import com.github.damontecres.wholphin.ui.main.HomeSection @@ -114,7 +115,7 @@ class RecommendedMovieViewModel HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"), HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"), HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"), - ) + ).filter { it.items.isNotEmpty() } withContext(Dispatchers.Main) { this@RecommendedMovieViewModel.rows.value = homeRows loading.value = LoadingState.Success @@ -131,6 +132,7 @@ fun RecommendedMovie( preferences: UserPreferences, parentId: UUID, onClickItem: (BaseItem) -> Unit, + onFocusPosition: (RowColumn) -> Unit, modifier: Modifier = Modifier, viewModel: RecommendedMovieViewModel = hiltViewModel(), ) { @@ -151,6 +153,7 @@ fun RecommendedMovie( HomePageContent( homeRows = rows, onClickItem = onClickItem, + onFocusPosition = onFocusPosition, modifier = modifier, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt index 96117e4d..d7e32f83 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt @@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomeRow import com.github.damontecres.wholphin.ui.main.HomeSection @@ -126,7 +127,7 @@ class RecommendedTvShowViewModel HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"), HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"), HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"), - ) + ).filter { it.items.isNotEmpty() } withContext(Dispatchers.Main) { this@RecommendedTvShowViewModel.rows.value = homeRows loading.value = LoadingState.Success @@ -143,6 +144,7 @@ fun RecommendedTvShow( preferences: UserPreferences, parentId: UUID, onClickItem: (BaseItem) -> Unit, + onFocusPosition: (RowColumn) -> Unit, modifier: Modifier = Modifier, viewModel: RecommendedTvShowViewModel = hiltViewModel(), ) { @@ -163,6 +165,7 @@ fun RecommendedTvShow( HomePageContent( homeRows = rows, onClickItem = onClickItem, + onFocusPosition = onFocusPosition, modifier = modifier, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt index ea30bb9f..5de7a852 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt @@ -145,6 +145,9 @@ fun CollectionFolderMovie( preferences = preferences, onClickItem = onClickItem, parentId = destination.itemId, + onFocusPosition = { pos -> + showHeader = pos.row < 1 + }, modifier = Modifier .padding(start = 16.dp) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt index a6e5c89a..7cd0ddd0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt @@ -145,6 +145,9 @@ fun CollectionFolderTv( preferences = preferences, parentId = destination.itemId, onClickItem = onClickItem, + onFocusPosition = { pos -> + showHeader = pos.row < 1 + }, modifier = Modifier .padding(start = 16.dp) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt index 2eeb6eaa..d639d6d3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt @@ -75,11 +75,7 @@ abstract class LoadingItemViewModel( ) + Dispatchers.IO, ) { try { - val fetchedItem = api.userLibraryApi.getItem(itemId).content - withContext(Dispatchers.Main) { - item.value = BaseItem.from(fetchedItem, api) - loading.value = LoadingState.Success - } + fetchAndSetItem(itemId) } catch (e: Exception) { Timber.e(e, "Failed to load item $itemId") withContext(Dispatchers.Main) { @@ -89,4 +85,13 @@ abstract class LoadingItemViewModel( } } } + + open suspend fun fetchAndSetItem(itemId: UUID) = + withContext(Dispatchers.IO) { + val fetchedItem = api.userLibraryApi.getItem(itemId).content + withContext(Dispatchers.Main) { + item.value = BaseItem.from(fetchedItem, api) + loading.value = LoadingState.Success + } + } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt index ee3dd9a8..e109c7db 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -28,10 +29,10 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -68,7 +69,9 @@ import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.tryRequestFocus +import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState +import kotlinx.coroutines.launch import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration @@ -237,6 +240,7 @@ fun SeriesDetailsContent( overviewOnClick = overviewOnClick, playOnClick = playOnClick, watchOnClick = watchOnClick, + bringIntoViewRequester = bringIntoViewRequester, modifier = Modifier .fillMaxWidth(.6f) @@ -301,12 +305,13 @@ fun SeriesDetailsContent( fun SeriesDetailsHeader( series: BaseItem, played: Boolean, + bringIntoViewRequester: BringIntoViewRequester, overviewOnClick: () -> Unit, playOnClick: () -> Unit, watchOnClick: () -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current + val scope = rememberCoroutineScope() val dto = series.data val details = buildList { @@ -370,13 +375,27 @@ fun SeriesDetailsHeader( resume = Duration.ZERO, icon = Icons.Default.PlayArrow, onClick = { playOnClick.invoke() }, - modifier = Modifier, + modifier = + Modifier.onFocusChanged { + if (it.isFocused) { + scope.launch(ExceptionHandler()) { + bringIntoViewRequester.bringIntoView() + } + } + }, ) ExpandableFaButton( title = if (played) R.string.mark_unwatched else R.string.mark_watched, iconStringRes = if (played) R.string.fa_eye else R.string.fa_eye_slash, onClick = watchOnClick, - modifier = Modifier, + modifier = + Modifier.onFocusChanged { + if (it.isFocused) { + scope.launch(ExceptionHandler()) { + bringIntoViewRequester.bringIntoView() + } + } + }, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt index ffea0452..e7cc3670 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt @@ -235,7 +235,7 @@ class SeriesViewModel } else { api.playStateApi.markUnplayedItem(seriesId) } - init(prefs, seriesId, null, null, null) + fetchItem(seriesId, null) } fun refreshEpisode( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 83ba1211..ed9a769d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -123,7 +123,7 @@ class MovieViewModel } else { api.playStateApi.markUnplayedItem(itemId) } - init(itemId, null) + fetchAndSetItem(itemId) } fun savePlayVersion( @@ -377,7 +377,7 @@ fun MovieDetailsContent( bringIntoViewRequester = bringIntoViewRequester, overviewOnClick = overviewOnClick, Modifier - .fillMaxWidth(.7f) + .fillMaxWidth(.75f) .padding(bottom = 8.dp), ) ExpandablePlayButtons( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt index 1c0963ec..74faaccf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt @@ -1,5 +1,7 @@ package com.github.damontecres.wholphin.ui.detail.movie +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,6 +12,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -63,7 +67,7 @@ fun MovieDetailsHeader( text = movie.name ?: "", color = MaterialTheme.colorScheme.onSurface, style = - MaterialTheme.typography.displayLarge.copy( + MaterialTheme.typography.displayMedium.copy( shadow = Shadow( color = Color.DarkGray, @@ -116,13 +120,26 @@ fun MovieDetailsHeader( } } + dto.taglines?.firstOrNull()?.let { tagline -> + Text( + text = tagline, + style = MaterialTheme.typography.bodyLarge, + ) + } + // Description dto.overview?.let { overview -> + val interactionSource = remember { MutableInteractionSource() } + val focused = interactionSource.collectIsFocusedAsState().value + LaunchedEffect(focused) { + if (focused) bringIntoViewRequester.bringIntoView() + } OverviewText( overview = overview, maxLines = 3, onClick = overviewOnClick, textBoxHeight = Dp.Unspecified, + interactionSource = interactionSource, ) } movie.data.people diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 5f9a1923..e5c8975a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -99,6 +100,7 @@ fun HomePageContent( homeRows: List, onClickItem: (BaseItem) -> Unit, modifier: Modifier = Modifier, + onFocusPosition: ((RowColumn) -> Unit)? = null, ) { var position by rememberSaveable(stateSaver = RowColumnSaver) { mutableStateOf(RowColumn(0, 0)) @@ -198,7 +200,16 @@ fun HomePageContent( ).ifElse( RowColumn(rowIndex, index) == position, Modifier.focusRequester(positionFocusRequester), - ), + ).onFocusChanged { + if (it.isFocused) { + onFocusPosition?.invoke( + RowColumn( + rowIndex, + index, + ), + ) + } + }, interactionSource = null, cardHeight = Cards.height2x3, )