Updates to chapter card UI (#1729)

## Description
Changes the chapter cards to use the chapter's image's aspect ratio
instead of forcing the video's aspect ratio. These ratios can be
different sometimes often for "dummy" chapters.

Also, the font size for the chapter name & position is reduced a bit.

### Related issues
N/A

### Testing
Emulator

## Screenshots
<img width="873" height="193" alt="image"
src="https://github.com/user-attachments/assets/e1c0e8a8-2e37-4644-bc99-899e20c4af09"
/>

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-07-21 17:21:07 -04:00 • committed by GitHub
parent 0a4ab7a547
commit 48005ce67a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 57 additions and 47 deletions

View file

@ -1,38 +1,30 @@
package com.github.damontecres.wholphin.data.model package com.github.damontecres.wholphin.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.BaseItemDto
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID
import kotlin.time.Duration import kotlin.time.Duration
/** /**
* Represents a chapter within a video * Represents a chapter within a video
*/ */
data class Chapter( data class Chapter(
val itemId: UUID,
val name: String?, val name: String?,
val position: Duration, val position: Duration,
val imageUrl: String?, val tag: String?,
val index: Int,
) { ) {
companion object { companion object {
fun fromDto( fun fromDto(dto: BaseItemDto): List<Chapter> =
dto: BaseItemDto,
api: ApiClient,
): List<Chapter> =
dto.chapters dto.chapters
?.mapIndexed { index, chapter -> ?.mapIndexed { index, chapter ->
Chapter( Chapter(
chapter.name, itemId = dto.id,
chapter.startPositionTicks.ticks, name = chapter.name,
chapter.imageTag?.let { position = chapter.startPositionTicks.ticks,
api.imageApi.getItemImageUrl( tag = chapter.imageTag,
itemId = dto.id, index = index,
imageType = ImageType.CHAPTER,
tag = it,
imageIndex = index,
)
},
) )
}?.sortedBy { it.position } }?.sortedBy { it.position }
.orEmpty() .orEmpty()

View file

@ -5,42 +5,62 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.text.font.FontWeight
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.compose.ui.unit.times
import androidx.tv.material3.Card import androidx.tv.material3.Card
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.data.model.Chapter
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.formatDuration import com.github.damontecres.wholphin.ui.formatDuration
import com.github.damontecres.wholphin.ui.roundSeconds import com.github.damontecres.wholphin.ui.roundSeconds
import kotlin.time.Duration import org.jellyfin.sdk.model.api.ImageType
/** /**
* Card for a [com.github.damontecres.wholphin.data.model.Chapter] * Card for a [com.github.damontecres.wholphin.data.model.Chapter]
*/ */
@Composable @Composable
fun ChapterCard( fun ChapterCard(
name: String?, chapter: Chapter,
position: Duration,
imageUrl: String?,
onClick: () -> Unit, onClick: () -> Unit,
aspectRatio: Float,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
cardHeight: Dp = 120.dp, cardHeight: Dp = 120.dp,
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null,
) { ) {
val density = LocalDensity.current
val imageUrlService = LocalImageUrlService.current
val imageUrl =
remember(chapter, cardHeight) {
imageUrlService.getItemImageUrl(
itemId = chapter.itemId,
imageType = ImageType.CHAPTER,
tag = chapter.tag,
imageIndex = chapter.index,
fillHeight = with(density) { cardHeight.roundToPx() },
)
}
var width by remember { mutableStateOf(AspectRatios.WIDE * cardHeight) }
Card( Card(
modifier = modifier.size(cardHeight * aspectRatio, cardHeight), modifier = modifier.height(cardHeight),
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
interactionSource = interactionSource, interactionSource = interactionSource,
@ -51,24 +71,36 @@ fun ChapterCard(
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onSuccess = {
width =
with(density) {
it.painter.intrinsicSize.width
.toDp()
}
},
) )
Box( Box(
modifier = modifier =
Modifier Modifier
.width(width)
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.fillMaxWidth()
.background(AppColors.TransparentBlack50), .background(AppColors.TransparentBlack50),
) { ) {
Column(modifier = Modifier.padding(4.dp)) { Column(modifier = Modifier.padding(4.dp)) {
name?.let { chapter.name?.let {
Text( Text(
text = it, text = it,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold,
) )
} }
val resources = LocalResources.current val resources = LocalResources.current
val positionText = remember(position) { resources.formatDuration(position.roundSeconds) } val positionText =
remember(chapter.position) { resources.formatDuration(chapter.position.roundSeconds) }
Text( Text(
text = positionText, text = positionText,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Normal,
) )
} }
} }

View file

@ -21,7 +21,6 @@ import com.github.damontecres.wholphin.data.model.Chapter
@Composable @Composable
fun ChapterRow( fun ChapterRow(
chapters: List<Chapter>, chapters: List<Chapter>,
aspectRatio: Float,
onClick: (Chapter) -> Unit, onClick: (Chapter) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onLongClick: ((Chapter) -> Unit)? = null, onLongClick: ((Chapter) -> Unit)? = null,
@ -47,11 +46,8 @@ fun ChapterRow(
) { ) {
itemsIndexed(chapters) { index, item -> itemsIndexed(chapters) { index, item ->
ChapterCard( ChapterCard(
name = item.name, chapter = item,
position = item.position,
imageUrl = item.imageUrl,
onClick = { onClick(item) }, onClick = { onClick(item) },
aspectRatio = aspectRatio,
modifier = Modifier, modifier = Modifier,
onLongClick = onLongClick?.let { { it(item) } }, onLongClick = onLongClick?.let { { it(item) } },
) )

View file

@ -33,10 +33,8 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.services.TrailerService
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.cards.ChapterRow import com.github.damontecres.wholphin.ui.cards.ChapterRow
@ -412,7 +410,6 @@ fun MovieDetailsContent(
item { item {
ChapterRow( ChapterRow(
chapters = chapters, chapters = chapters,
aspectRatio = movie.data.aspectRatioFloat ?: AspectRatios.WIDE,
onClick = { onClick = {
position = CHAPTER_ROW position = CHAPTER_ROW
playOnClick.invoke(it.position) playOnClick.invoke(it.position)

View file

@ -117,7 +117,7 @@ class MovieViewModel
userPreferencesService.getCurrent(), userPreferencesService.getCurrent(),
) )
val remoteTrailers = trailerService.getRemoteTrailers(movie) val remoteTrailers = trailerService.getRemoteTrailers(movie)
val chapters = Chapter.fromDto(movie.data, api) val chapters = Chapter.fromDto(movie.data)
_state.update { _state.update {
it.copy( it.copy(
loading = DataLoadingState.Success(movie), loading = DataLoadingState.Success(movie),

View file

@ -570,7 +570,7 @@ class PlaybackViewModel
} }
} }
val chapters = Chapter.fromDto(base, api) val chapters = Chapter.fromDto(base)
_state.update { _state.update {
it.copy(currentItemPlayback = itemPlaybackToUse) it.copy(currentItemPlayback = itemPlaybackToUse)
} }

View file

@ -44,7 +44,6 @@ fun ChapterRowOverlay(
controllerViewState: ControllerViewState, controllerViewState: ControllerViewState,
chapters: List<Chapter>, chapters: List<Chapter>,
hasNext: Boolean, hasNext: Boolean,
aspectRatio: Float,
onChangeState: (OverlayViewState) -> Unit, onChangeState: (OverlayViewState) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -115,10 +114,7 @@ fun ChapterRowOverlay(
if (isFocused) controllerViewState.pulseControls() if (isFocused) controllerViewState.pulseControls()
} }
ChapterCard( ChapterCard(
name = chapter.name, chapter = chapter,
position = chapter.position,
imageUrl = chapter.imageUrl,
aspectRatio = aspectRatio,
onClick = { onClick = {
player.seekTo(chapter.position.inWholeMilliseconds) player.seekTo(chapter.position.inWholeMilliseconds)
controllerViewState.hideControls() controllerViewState.hideControls()

View file

@ -52,9 +52,7 @@ import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.Chapter
import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.LocalImageUrlService import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.components.TimeDisplay import com.github.damontecres.wholphin.ui.components.TimeDisplay
import com.github.damontecres.wholphin.ui.formatDuration import com.github.damontecres.wholphin.ui.formatDuration
@ -228,7 +226,6 @@ fun PlaybackOverlay(
controllerViewState = controllerViewState, controllerViewState = controllerViewState,
chapters = chapters, chapters = chapters,
hasNext = nextEnabled, hasNext = nextEnabled,
aspectRatio = item?.data?.aspectRatioFloat ?: AspectRatios.WIDE,
onChangeState = onChangeState, onChangeState = onChangeState,
modifier = modifier =
Modifier Modifier