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.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,
)
}

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.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,
)
}

View file

@ -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)

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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()
}
}
},
)
}
}

View file

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

View file

@ -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(

View file

@ -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

View file

@ -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<HomeRow>,
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,
)