mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-06 18:54:49 +01:00
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:
parent
0a4ab7a547
commit
48005ce67a
8 changed files with 57 additions and 47 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) } },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue