Various UI updates (#35)

Fixes and tweaks several UI issues
This commit is contained in:
damontecres 2025-10-19 00:08:21 -04:00 committed by GitHub
parent 82111b99e4
commit bb19ba316b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 80 additions and 16 deletions

View file

@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields 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.HomePageContent
import com.github.damontecres.wholphin.ui.main.HomeRow import com.github.damontecres.wholphin.ui.main.HomeRow
import com.github.damontecres.wholphin.ui.main.HomeSection import com.github.damontecres.wholphin.ui.main.HomeSection
@ -114,7 +115,7 @@ class RecommendedMovieViewModel
HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"), HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"),
HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"), HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"),
HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"), HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"),
) ).filter { it.items.isNotEmpty() }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@RecommendedMovieViewModel.rows.value = homeRows this@RecommendedMovieViewModel.rows.value = homeRows
loading.value = LoadingState.Success loading.value = LoadingState.Success
@ -131,6 +132,7 @@ fun RecommendedMovie(
preferences: UserPreferences, preferences: UserPreferences,
parentId: UUID, parentId: UUID,
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
onFocusPosition: (RowColumn) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: RecommendedMovieViewModel = hiltViewModel(), viewModel: RecommendedMovieViewModel = hiltViewModel(),
) { ) {
@ -151,6 +153,7 @@ fun RecommendedMovie(
HomePageContent( HomePageContent(
homeRows = rows, homeRows = rows,
onClickItem = onClickItem, onClickItem = onClickItem,
onFocusPosition = onFocusPosition,
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields 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.HomePageContent
import com.github.damontecres.wholphin.ui.main.HomeRow import com.github.damontecres.wholphin.ui.main.HomeRow
import com.github.damontecres.wholphin.ui.main.HomeSection import com.github.damontecres.wholphin.ui.main.HomeSection
@ -126,7 +127,7 @@ class RecommendedTvShowViewModel
HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"), HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"),
HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"), HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"),
HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"), HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"),
) ).filter { it.items.isNotEmpty() }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@RecommendedTvShowViewModel.rows.value = homeRows this@RecommendedTvShowViewModel.rows.value = homeRows
loading.value = LoadingState.Success loading.value = LoadingState.Success
@ -143,6 +144,7 @@ fun RecommendedTvShow(
preferences: UserPreferences, preferences: UserPreferences,
parentId: UUID, parentId: UUID,
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
onFocusPosition: (RowColumn) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: RecommendedTvShowViewModel = hiltViewModel(), viewModel: RecommendedTvShowViewModel = hiltViewModel(),
) { ) {
@ -163,6 +165,7 @@ fun RecommendedTvShow(
HomePageContent( HomePageContent(
homeRows = rows, homeRows = rows,
onClickItem = onClickItem, onClickItem = onClickItem,
onFocusPosition = onFocusPosition,
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -145,6 +145,9 @@ fun CollectionFolderMovie(
preferences = preferences, preferences = preferences,
onClickItem = onClickItem, onClickItem = onClickItem,
parentId = destination.itemId, parentId = destination.itemId,
onFocusPosition = { pos ->
showHeader = pos.row < 1
},
modifier = modifier =
Modifier Modifier
.padding(start = 16.dp) .padding(start = 16.dp)

View file

@ -145,6 +145,9 @@ fun CollectionFolderTv(
preferences = preferences, preferences = preferences,
parentId = destination.itemId, parentId = destination.itemId,
onClickItem = onClickItem, onClickItem = onClickItem,
onFocusPosition = { pos ->
showHeader = pos.row < 1
},
modifier = modifier =
Modifier Modifier
.padding(start = 16.dp) .padding(start = 16.dp)

View file

@ -75,11 +75,7 @@ abstract class LoadingItemViewModel(
) + Dispatchers.IO, ) + Dispatchers.IO,
) { ) {
try { try {
val fetchedItem = api.userLibraryApi.getItem(itemId).content fetchAndSetItem(itemId)
withContext(Dispatchers.Main) {
item.value = BaseItem.from(fetchedItem, api)
loading.value = LoadingState.Success
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to load item $itemId") Timber.e(e, "Failed to load item $itemId")
withContext(Dispatchers.Main) { 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
}
}
} }

View file

@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.draw.drawWithContent
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale 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.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel 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.rememberPosition
import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration import kotlin.time.Duration
@ -237,6 +240,7 @@ fun SeriesDetailsContent(
overviewOnClick = overviewOnClick, overviewOnClick = overviewOnClick,
playOnClick = playOnClick, playOnClick = playOnClick,
watchOnClick = watchOnClick, watchOnClick = watchOnClick,
bringIntoViewRequester = bringIntoViewRequester,
modifier = modifier =
Modifier Modifier
.fillMaxWidth(.6f) .fillMaxWidth(.6f)
@ -301,12 +305,13 @@ fun SeriesDetailsContent(
fun SeriesDetailsHeader( fun SeriesDetailsHeader(
series: BaseItem, series: BaseItem,
played: Boolean, played: Boolean,
bringIntoViewRequester: BringIntoViewRequester,
overviewOnClick: () -> Unit, overviewOnClick: () -> Unit,
playOnClick: () -> Unit, playOnClick: () -> Unit,
watchOnClick: () -> Unit, watchOnClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val scope = rememberCoroutineScope()
val dto = series.data val dto = series.data
val details = val details =
buildList { buildList {
@ -370,13 +375,27 @@ fun SeriesDetailsHeader(
resume = Duration.ZERO, resume = Duration.ZERO,
icon = Icons.Default.PlayArrow, icon = Icons.Default.PlayArrow,
onClick = { playOnClick.invoke() }, onClick = { playOnClick.invoke() },
modifier = Modifier, modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
) )
ExpandableFaButton( ExpandableFaButton(
title = if (played) R.string.mark_unwatched else R.string.mark_watched, 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, iconStringRes = if (played) R.string.fa_eye else R.string.fa_eye_slash,
onClick = watchOnClick, onClick = watchOnClick,
modifier = Modifier, modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
) )
} }
} }

View file

@ -235,7 +235,7 @@ class SeriesViewModel
} else { } else {
api.playStateApi.markUnplayedItem(seriesId) api.playStateApi.markUnplayedItem(seriesId)
} }
init(prefs, seriesId, null, null, null) fetchItem(seriesId, null)
} }
fun refreshEpisode( fun refreshEpisode(

View file

@ -123,7 +123,7 @@ class MovieViewModel
} else { } else {
api.playStateApi.markUnplayedItem(itemId) api.playStateApi.markUnplayedItem(itemId)
} }
init(itemId, null) fetchAndSetItem(itemId)
} }
fun savePlayVersion( fun savePlayVersion(
@ -377,7 +377,7 @@ fun MovieDetailsContent(
bringIntoViewRequester = bringIntoViewRequester, bringIntoViewRequester = bringIntoViewRequester,
overviewOnClick = overviewOnClick, overviewOnClick = overviewOnClick,
Modifier Modifier
.fillMaxWidth(.7f) .fillMaxWidth(.75f)
.padding(bottom = 8.dp), .padding(bottom = 8.dp),
) )
ExpandablePlayButtons( ExpandablePlayButtons(

View file

@ -1,5 +1,7 @@
package com.github.damontecres.wholphin.ui.detail.movie 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.layout.widthIn
import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@ -63,7 +67,7 @@ fun MovieDetailsHeader(
text = movie.name ?: "", text = movie.name ?: "",
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = style =
MaterialTheme.typography.displayLarge.copy( MaterialTheme.typography.displayMedium.copy(
shadow = shadow =
Shadow( Shadow(
color = Color.DarkGray, color = Color.DarkGray,
@ -116,13 +120,26 @@ fun MovieDetailsHeader(
} }
} }
dto.taglines?.firstOrNull()?.let { tagline ->
Text(
text = tagline,
style = MaterialTheme.typography.bodyLarge,
)
}
// Description // Description
dto.overview?.let { overview -> dto.overview?.let { overview ->
val interactionSource = remember { MutableInteractionSource() }
val focused = interactionSource.collectIsFocusedAsState().value
LaunchedEffect(focused) {
if (focused) bringIntoViewRequester.bringIntoView()
}
OverviewText( OverviewText(
overview = overview, overview = overview,
maxLines = 3, maxLines = 3,
onClick = overviewOnClick, onClick = overviewOnClick,
textBoxHeight = Dp.Unspecified, textBoxHeight = Dp.Unspecified,
interactionSource = interactionSource,
) )
} }
movie.data.people movie.data.people

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@ -99,6 +100,7 @@ fun HomePageContent(
homeRows: List<HomeRow>, homeRows: List<HomeRow>,
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onFocusPosition: ((RowColumn) -> Unit)? = null,
) { ) {
var position by rememberSaveable(stateSaver = RowColumnSaver) { var position by rememberSaveable(stateSaver = RowColumnSaver) {
mutableStateOf(RowColumn(0, 0)) mutableStateOf(RowColumn(0, 0))
@ -198,7 +200,16 @@ fun HomePageContent(
).ifElse( ).ifElse(
RowColumn(rowIndex, index) == position, RowColumn(rowIndex, index) == position,
Modifier.focusRequester(positionFocusRequester), Modifier.focusRequester(positionFocusRequester),
), ).onFocusChanged {
if (it.isFocused) {
onFocusPosition?.invoke(
RowColumn(
rowIndex,
index,
),
)
}
},
interactionSource = null, interactionSource = null,
cardHeight = Cards.height2x3, cardHeight = Cards.height2x3,
) )