From 48005ce67af296fe86af279aff8ec81e4ae06308 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 21 Jul 2026 17:21:07 -0400 Subject: [PATCH] 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 image ## AI or LLM usage None --- .../wholphin/data/model/Chapter.kt | 28 ++++------ .../wholphin/ui/cards/ChapterCard.kt | 54 +++++++++++++++---- .../wholphin/ui/cards/ChapterRow.kt | 6 +-- .../wholphin/ui/detail/movie/MovieDetails.kt | 3 -- .../ui/detail/movie/MovieViewModel.kt | 2 +- .../wholphin/ui/playback/PlaybackViewModel.kt | 2 +- .../ui/playback/overlay/ChapterRowOverlay.kt | 6 +-- .../ui/playback/overlay/PlaybackOverlay.kt | 3 -- 8 files changed, 57 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt index 3e07964c..71a300fb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt @@ -1,38 +1,30 @@ 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.ImageType import org.jellyfin.sdk.model.extensions.ticks +import java.util.UUID import kotlin.time.Duration /** * Represents a chapter within a video */ data class Chapter( + val itemId: UUID, val name: String?, val position: Duration, - val imageUrl: String?, + val tag: String?, + val index: Int, ) { companion object { - fun fromDto( - dto: BaseItemDto, - api: ApiClient, - ): List = + fun fromDto(dto: BaseItemDto): List = 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, - ) - }, + itemId = dto.id, + name = chapter.name, + position = chapter.startPositionTicks.ticks, + tag = chapter.imageTag, + index = index, ) }?.sortedBy { it.position } .orEmpty() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterCard.kt index d3c87124..634cc988 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterCard.kt @@ -5,42 +5,62 @@ 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.height 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.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity 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.times import androidx.tv.material3.Card +import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text 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.AspectRatios +import com.github.damontecres.wholphin.ui.LocalImageUrlService import com.github.damontecres.wholphin.ui.formatDuration 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] */ @Composable fun ChapterCard( - name: String?, - position: Duration, - imageUrl: String?, + chapter: Chapter, onClick: () -> Unit, - aspectRatio: Float, modifier: Modifier = Modifier, cardHeight: Dp = 120.dp, onLongClick: (() -> Unit)? = 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( - modifier = modifier.size(cardHeight * aspectRatio, cardHeight), + modifier = modifier.height(cardHeight), onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource, @@ -51,24 +71,36 @@ fun ChapterCard( contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize(), + onSuccess = { + width = + with(density) { + it.painter.intrinsicSize.width + .toDp() + } + }, ) Box( modifier = Modifier + .width(width) .align(Alignment.BottomStart) - .fillMaxWidth() .background(AppColors.TransparentBlack50), ) { Column(modifier = Modifier.padding(4.dp)) { - name?.let { + chapter.name?.let { Text( text = it, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, ) } val resources = LocalResources.current - val positionText = remember(position) { resources.formatDuration(position.roundSeconds) } + val positionText = + remember(chapter.position) { resources.formatDuration(chapter.position.roundSeconds) } Text( text = positionText, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Normal, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt index b1e64a2a..5fa6316f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt @@ -21,7 +21,6 @@ import com.github.damontecres.wholphin.data.model.Chapter @Composable fun ChapterRow( chapters: List, - aspectRatio: Float, onClick: (Chapter) -> Unit, modifier: Modifier = Modifier, onLongClick: ((Chapter) -> Unit)? = null, @@ -47,11 +46,8 @@ fun ChapterRow( ) { itemsIndexed(chapters) { index, item -> ChapterCard( - name = item.name, - position = item.position, - imageUrl = item.imageUrl, + chapter = item, onClick = { onClick(item) }, - aspectRatio = aspectRatio, modifier = Modifier, onLongClick = onLongClick?.let { { it(item) } }, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index bc4a3461..db4608ac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -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.Person 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.services.TrailerService -import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus import com.github.damontecres.wholphin.ui.cards.ChapterRow @@ -412,7 +410,6 @@ fun MovieDetailsContent( item { ChapterRow( chapters = chapters, - aspectRatio = movie.data.aspectRatioFloat ?: AspectRatios.WIDE, onClick = { position = CHAPTER_ROW playOnClick.invoke(it.position) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index 9df28425..49a7d742 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -117,7 +117,7 @@ class MovieViewModel userPreferencesService.getCurrent(), ) val remoteTrailers = trailerService.getRemoteTrailers(movie) - val chapters = Chapter.fromDto(movie.data, api) + val chapters = Chapter.fromDto(movie.data) _state.update { it.copy( loading = DataLoadingState.Success(movie), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index fae25907..2dc01d36 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -570,7 +570,7 @@ class PlaybackViewModel } } - val chapters = Chapter.fromDto(base, api) + val chapters = Chapter.fromDto(base) _state.update { it.copy(currentItemPlayback = itemPlaybackToUse) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/ChapterRowOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/ChapterRowOverlay.kt index 45e335ab..fa32950b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/ChapterRowOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/ChapterRowOverlay.kt @@ -44,7 +44,6 @@ fun ChapterRowOverlay( controllerViewState: ControllerViewState, chapters: List, hasNext: Boolean, - aspectRatio: Float, onChangeState: (OverlayViewState) -> Unit, modifier: Modifier = Modifier, ) { @@ -115,10 +114,7 @@ fun ChapterRowOverlay( if (isFocused) controllerViewState.pulseControls() } ChapterCard( - name = chapter.name, - position = chapter.position, - imageUrl = chapter.imageUrl, - aspectRatio = aspectRatio, + chapter = chapter, onClick = { player.seekTo(chapter.position.inWholeMilliseconds) controllerViewState.hideControls() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackOverlay.kt index a6ade035..9af4a171 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackOverlay.kt @@ -52,9 +52,7 @@ import coil3.compose.AsyncImage import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter 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.AspectRatios import com.github.damontecres.wholphin.ui.LocalImageUrlService import com.github.damontecres.wholphin.ui.components.TimeDisplay import com.github.damontecres.wholphin.ui.formatDuration @@ -228,7 +226,6 @@ fun PlaybackOverlay( controllerViewState = controllerViewState, chapters = chapters, hasNext = nextEnabled, - aspectRatio = item?.data?.aspectRatioFloat ?: AspectRatios.WIDE, onChangeState = onChangeState, modifier = Modifier