mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Show chapters on movie details
This commit is contained in:
parent
de36eb7072
commit
9a588f38b4
6 changed files with 262 additions and 52 deletions
|
|
@ -0,0 +1,36 @@
|
|||
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.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class Chapter(
|
||||
val name: String?,
|
||||
val position: Duration,
|
||||
val imageUrl: String?,
|
||||
) {
|
||||
companion object {
|
||||
fun fromDto(
|
||||
dto: BaseItemDto,
|
||||
api: ApiClient,
|
||||
): List<Chapter> =
|
||||
dto.chapters
|
||||
?.mapIndexed { index, chapter ->
|
||||
Chapter(
|
||||
chapter.name,
|
||||
chapter.startPositionTicks.ticks,
|
||||
chapter.imageTag?.let {
|
||||
api.imageApi.getItemImageUrl(
|
||||
itemId = dto.id,
|
||||
imageType = ImageType.CHAPTER,
|
||||
tag = it,
|
||||
imageIndex = index,
|
||||
)
|
||||
},
|
||||
)
|
||||
}.orEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.github.damontecres.dolphin.ui.cards
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Card
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.dolphin.ui.AppColors
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Composable
|
||||
fun ChapterCard(
|
||||
name: String?,
|
||||
position: Duration,
|
||||
imageUrl: String?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
cardHeight: Dp = 140.dp,
|
||||
aspectRatio: Float = 16f / 9,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.size(cardHeight * aspectRatio, cardHeight),
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.TransparentBlack50),
|
||||
) {
|
||||
name?.let {
|
||||
Text(
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = position.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.github.damontecres.dolphin.ui.cards
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.dolphin.data.model.Chapter
|
||||
|
||||
@Composable
|
||||
fun ChapterRow(
|
||||
chapters: List<Chapter>,
|
||||
onClick: (Chapter) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongClick: ((Chapter) -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "Chapters",
|
||||
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(chapters) { index, item ->
|
||||
ChapterCard(
|
||||
name = item.name,
|
||||
position = item.position,
|
||||
imageUrl = item.imageUrl,
|
||||
onClick = { onClick(item) },
|
||||
modifier = Modifier,
|
||||
onLongClick = onLongClick?.let { { it(item) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.github.damontecres.dolphin.ui.cards
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.dolphin.data.model.Person
|
||||
|
||||
@Composable
|
||||
fun PersonRow(
|
||||
people: List<Person>,
|
||||
onClick: (Person) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongClick: ((Person) -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
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 = { onClick.invoke(item) },
|
||||
onLongClick = { onLongClick?.invoke(item) },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,10 @@ 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.fillMaxSize
|
||||
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
|
||||
|
|
@ -25,7 +23,6 @@ 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
|
||||
|
|
@ -34,13 +31,14 @@ 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.Chapter
|
||||
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.cards.ChapterRow
|
||||
import com.github.damontecres.dolphin.ui.cards.PersonRow
|
||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons
|
||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
|
|
@ -68,6 +66,7 @@ class MovieViewModel
|
|||
api: ApiClient,
|
||||
) : LoadingItemViewModel<Video>(api) {
|
||||
val people = MutableLiveData<List<Person>>(listOf())
|
||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||
|
||||
override fun init(
|
||||
itemId: UUID,
|
||||
|
|
@ -80,6 +79,7 @@ class MovieViewModel
|
|||
item.data.people?.letNotEmpty { people ->
|
||||
people.map { Person.fromDto(it, api) }
|
||||
}
|
||||
chapters.value = Chapter.fromDto(item.data, api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,17 +97,28 @@ fun MovieDetails(
|
|||
}
|
||||
val item by viewModel.item.observeAsState()
|
||||
val people by viewModel.people.observeAsState(listOf())
|
||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
item?.let {
|
||||
item?.let { movie ->
|
||||
MovieDetailsContent(
|
||||
preferences = preferences,
|
||||
navigationManager = navigationManager,
|
||||
movie = it,
|
||||
movie = movie,
|
||||
people = people,
|
||||
chapters = chapters,
|
||||
playOnClick = {
|
||||
navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
movie.id,
|
||||
it.inWholeMilliseconds,
|
||||
movie,
|
||||
),
|
||||
)
|
||||
},
|
||||
overviewOnClick = {},
|
||||
moreOnClick = {},
|
||||
watchOnClick = {},
|
||||
|
|
@ -124,6 +135,8 @@ fun MovieDetailsContent(
|
|||
navigationManager: NavigationManager,
|
||||
movie: BaseItem,
|
||||
people: List<Person>,
|
||||
chapters: List<Chapter>,
|
||||
playOnClick: (Duration) -> Unit,
|
||||
overviewOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
|
|
@ -171,14 +184,17 @@ fun MovieDetailsContent(
|
|||
)
|
||||
}
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(32.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
item {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester),
|
||||
modifier =
|
||||
Modifier
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
.padding(bottom = 120.dp),
|
||||
) {
|
||||
MovieDetailsHeader(
|
||||
movie = movie,
|
||||
|
|
@ -190,15 +206,7 @@ fun MovieDetailsContent(
|
|||
ExpandablePlayButtons(
|
||||
resumePosition = resumePosition,
|
||||
watched = dto.userData?.played ?: false,
|
||||
playOnClick = {
|
||||
navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
movie.id,
|
||||
it.inWholeMilliseconds,
|
||||
movie,
|
||||
),
|
||||
)
|
||||
},
|
||||
playOnClick = playOnClick,
|
||||
moreOnClick = moreOnClick,
|
||||
watchOnClick = watchOnClick,
|
||||
buttonOnFocusChanged = {
|
||||
|
|
@ -214,38 +222,22 @@ fun MovieDetailsContent(
|
|||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
PersonRow(
|
||||
people = people,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (chapters.isNotEmpty()) {
|
||||
item {
|
||||
ChapterRow(
|
||||
chapters = chapters,
|
||||
onClick = { playOnClick.invoke(it.position) },
|
||||
onLongClick = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.github.damontecres.dolphin.data.model.Chapter
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||
|
|
@ -80,6 +81,7 @@ class PlaybackViewModel
|
|||
val subtitleStreams = MutableLiveData<List<SubtitleStream>>(listOf())
|
||||
val currentPlayback = MutableLiveData<CurrentPlayback?>(null)
|
||||
val trickplay = MutableLiveData<TrickplayInfo?>(null)
|
||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||
|
||||
private lateinit var deviceProfile: DeviceProfile
|
||||
private lateinit var dto: BaseItemDto
|
||||
|
|
@ -174,6 +176,9 @@ class PlaybackViewModel
|
|||
if (destination.positionMs > 0) destination.positionMs else C.TIME_UNSET,
|
||||
)
|
||||
player.prepare()
|
||||
|
||||
this@PlaybackViewModel.chapters.value = Chapter.fromDto(dto, api)
|
||||
Timber.v("chapters=${this@PlaybackViewModel.chapters.value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue