mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add people to movie details
This commit is contained in:
parent
0595c7174c
commit
ba92001412
8 changed files with 286 additions and 50 deletions
|
|
@ -0,0 +1,30 @@
|
|||
package com.github.damontecres.dolphin.data.model
|
||||
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.PersonKind
|
||||
|
||||
data class Person(
|
||||
val id: UUID,
|
||||
val name: String?,
|
||||
val role: String?,
|
||||
val type: PersonKind,
|
||||
val imageUrl: String?,
|
||||
) {
|
||||
companion object {
|
||||
fun fromDto(
|
||||
dto: BaseItemPerson,
|
||||
api: ApiClient,
|
||||
): Person =
|
||||
Person(
|
||||
id = dto.id,
|
||||
name = dto.name,
|
||||
role = dto.role,
|
||||
type = dto.type,
|
||||
imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,11 +32,12 @@ fun ItemRow(
|
|||
onLongClickItem: (BaseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
cardContent: @Composable (
|
||||
index: Int,
|
||||
item: BaseItem?,
|
||||
modifier: Modifier,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
) -> Unit = { item, modifier, onClick, onLongClick ->
|
||||
) -> Unit = { index, item, modifier, onClick, onLongClick ->
|
||||
ItemCard(
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
|
|
@ -86,6 +87,7 @@ fun ItemRow(
|
|||
cardOnFocus?.invoke(it.isFocused, index)
|
||||
}
|
||||
cardContent.invoke(
|
||||
index,
|
||||
item,
|
||||
cardModifier,
|
||||
{ if (item != null) onClickItem.invoke(item) },
|
||||
|
|
@ -112,7 +114,7 @@ fun BannerItemRow(
|
|||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
modifier = modifier,
|
||||
cardContent = { item, modifier, onClick, onLongClick ->
|
||||
cardContent = { index, item, modifier, onClick, onLongClick ->
|
||||
BannerCard(
|
||||
imageUrl = item?.imageUrl,
|
||||
aspectRatio =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
package com.github.damontecres.dolphin.ui.cards
|
||||
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Card
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.dolphin.data.model.Person
|
||||
import com.github.damontecres.dolphin.ui.enableMarquee
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun PersonCard(
|
||||
item: Person,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
cardWidth: Dp = 150.dp,
|
||||
cardHeight: Dp = 200.dp,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val hideOverlayDelay = 1_000L
|
||||
|
||||
val focused = interactionSource.collectIsFocusedAsState().value
|
||||
var focusedAfterDelay by remember { mutableStateOf(false) }
|
||||
|
||||
if (focused) {
|
||||
LaunchedEffect(Unit) {
|
||||
delay(hideOverlayDelay)
|
||||
if (focused) {
|
||||
focusedAfterDelay = true
|
||||
} else {
|
||||
focusedAfterDelay = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
focusedAfterDelay = false
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.width(cardWidth),
|
||||
) {
|
||||
ItemCardImage(
|
||||
imageUrl = item.imageUrl,
|
||||
name = item.name,
|
||||
showOverlay = true,
|
||||
favorite = false,
|
||||
watched = false,
|
||||
unwatchedCount = -1,
|
||||
watchedPercent = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(cardHeight),
|
||||
)
|
||||
Text(
|
||||
text = item.name ?: "",
|
||||
maxLines = 1,
|
||||
modifier =
|
||||
Modifier
|
||||
.enableMarquee(focusedAfterDelay),
|
||||
)
|
||||
item.role?.let {
|
||||
Text(
|
||||
text = item.role,
|
||||
maxLines = 1,
|
||||
modifier =
|
||||
Modifier
|
||||
.enableMarquee(focusedAfterDelay),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,7 +137,6 @@ fun ExpandablePlayButtons(
|
|||
) {
|
||||
if (resumePosition > Duration.ZERO) {
|
||||
item {
|
||||
// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
|
||||
ExpandablePlayButton(
|
||||
R.string.resume,
|
||||
resumePosition,
|
||||
|
|
@ -170,6 +169,7 @@ fun ExpandablePlayButtons(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Watched button
|
||||
item {
|
||||
ExpandableFaButton(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.github.damontecres.dolphin.ui.data
|
||||
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RowColumn(
|
||||
val row: Int,
|
||||
val column: Int,
|
||||
)
|
||||
|
||||
val RowColumnSaver =
|
||||
Saver<RowColumn, List<Int>>(
|
||||
save = { listOf(it.row, it.column) },
|
||||
restore = { RowColumn(it[0], it[1]) },
|
||||
)
|
||||
|
|
@ -2,11 +2,17 @@ package com.github.damontecres.dolphin.ui.detail.movie
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -19,30 +25,39 @@ 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.focusRestorer
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.data.model.Person
|
||||
import com.github.damontecres.dolphin.data.model.Video
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.cards.PersonCard
|
||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons
|
||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.dolphin.ui.detail.LoadingItemViewModel
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.letNotEmpty
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||
import com.github.damontecres.dolphin.util.LoadingState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
|
@ -51,7 +66,23 @@ class MovieViewModel
|
|||
@Inject
|
||||
constructor(
|
||||
api: ApiClient,
|
||||
) : LoadingItemViewModel<Video>(api)
|
||||
) : LoadingItemViewModel<Video>(api) {
|
||||
val people = MutableLiveData<List<Person>>(listOf())
|
||||
|
||||
override fun init(
|
||||
itemId: UUID,
|
||||
potential: BaseItem?,
|
||||
): Job? =
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
super.init(itemId, potential)?.join()
|
||||
item.value?.let { item ->
|
||||
people.value =
|
||||
item.data.people?.letNotEmpty { people ->
|
||||
people.map { Person.fromDto(it, api) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MovieDetails(
|
||||
|
|
@ -65,6 +96,7 @@ fun MovieDetails(
|
|||
viewModel.init(destination.itemId, destination.item)
|
||||
}
|
||||
val item by viewModel.item.observeAsState()
|
||||
val people by viewModel.people.observeAsState(listOf())
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
|
@ -72,10 +104,14 @@ fun MovieDetails(
|
|||
LoadingState.Success -> {
|
||||
item?.let {
|
||||
MovieDetailsContent(
|
||||
preferences,
|
||||
navigationManager,
|
||||
it,
|
||||
modifier,
|
||||
preferences = preferences,
|
||||
navigationManager = navigationManager,
|
||||
movie = it,
|
||||
people = people,
|
||||
overviewOnClick = {},
|
||||
moreOnClick = {},
|
||||
watchOnClick = {},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +123,10 @@ fun MovieDetailsContent(
|
|||
preferences: UserPreferences,
|
||||
navigationManager: NavigationManager,
|
||||
movie: BaseItem,
|
||||
people: List<Person>,
|
||||
overviewOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -97,7 +137,7 @@ fun MovieDetailsContent(
|
|||
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
// bringIntoViewRequester.bringIntoView()
|
||||
focusRequester.tryRequestFocus()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
|
|
@ -136,15 +176,17 @@ fun MovieDetailsContent(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
item {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester),
|
||||
) {
|
||||
MovieDetailsHeader(
|
||||
movie = movie,
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
overviewOnClick = {},
|
||||
overviewOnClick = overviewOnClick,
|
||||
Modifier
|
||||
.fillMaxWidth(.7f),
|
||||
)
|
||||
}
|
||||
item {
|
||||
ExpandablePlayButtons(
|
||||
resumePosition = resumePosition,
|
||||
watched = dto.userData?.played ?: false,
|
||||
|
|
@ -157,20 +199,55 @@ fun MovieDetailsContent(
|
|||
),
|
||||
)
|
||||
},
|
||||
moreOnClick = {
|
||||
// TODO
|
||||
},
|
||||
watchOnClick = {
|
||||
// TODO
|
||||
},
|
||||
moreOnClick = moreOnClick,
|
||||
watchOnClick = watchOnClick,
|
||||
buttonOnFocusChanged = {
|
||||
if (it.isFocused) {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (people.isNotEmpty()) {
|
||||
item {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 120.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "People",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
LazyRow(
|
||||
state = rememberLazyListState(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.focusRestorer(),
|
||||
) {
|
||||
itemsIndexed(people) { index, item ->
|
||||
PersonCard(
|
||||
item = item,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -44,6 +45,8 @@ import com.github.damontecres.dolphin.ui.cards.ItemRow
|
|||
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
|
||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.dolphin.ui.data.RowColumn
|
||||
import com.github.damontecres.dolphin.ui.data.RowColumnSaver
|
||||
import com.github.damontecres.dolphin.ui.ifElse
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||
|
|
@ -90,6 +93,10 @@ fun MainPageContent(
|
|||
homeRows: List<HomeRow>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
||||
mutableStateOf(RowColumn(0, 0))
|
||||
}
|
||||
// TODO use position instead?
|
||||
var focusedItem by remember {
|
||||
mutableStateOf<BaseItem?>(
|
||||
homeRows.getOrNull(0)?.items?.getOrNull(
|
||||
|
|
@ -98,7 +105,10 @@ fun MainPageContent(
|
|||
)
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { if (focusedItem != null) focusRequester.tryRequestFocus() }
|
||||
val positionFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
positionFocusRequester.tryRequestFocus()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
if (focusedItem?.backdropImageUrl.isNotNullOrBlank()) {
|
||||
val gradientColor = MaterialTheme.colorScheme.background
|
||||
|
|
@ -152,7 +162,7 @@ fun MainPageContent(
|
|||
),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
items(homeRows) { row ->
|
||||
itemsIndexed(homeRows) { rowIndex, row ->
|
||||
ItemRow(
|
||||
title = row.title ?: stringResource(row.section.nameRes),
|
||||
items = row.items,
|
||||
|
|
@ -160,11 +170,14 @@ fun MainPageContent(
|
|||
navigationManager.navigateTo(it.destination())
|
||||
},
|
||||
cardOnFocus = { isFocused, index ->
|
||||
if (isFocused) {
|
||||
focusedItem = row.items.getOrNull(index)
|
||||
position = RowColumn(rowIndex, index)
|
||||
}
|
||||
},
|
||||
onLongClickItem = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
cardContent = { item, modifier, onClick, onLongClick ->
|
||||
cardContent = { index, item, modifier, onClick, onLongClick ->
|
||||
// TODO better aspect ration handling?
|
||||
BannerCard(
|
||||
imageUrl = item?.imageUrl,
|
||||
|
|
@ -175,9 +188,13 @@ fun MainPageContent(
|
|||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier =
|
||||
modifier.ifElse(
|
||||
modifier
|
||||
.ifElse(
|
||||
focusedItem == item,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).ifElse(
|
||||
RowColumn(rowIndex, index) == position,
|
||||
Modifier.focusRequester(positionFocusRequester),
|
||||
),
|
||||
interactionSource = null,
|
||||
cardHeight = 200.dp,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ private val darkScheme =
|
|||
inverseSurface = inverseSurfaceDark,
|
||||
inverseOnSurface = inverseOnSurfaceDark,
|
||||
inversePrimary = inversePrimaryDark,
|
||||
border = inversePrimaryDark.copy(alpha = .5f),
|
||||
border = inversePrimaryDark.copy(alpha = .75f),
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue