mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Option to show title below cards
This commit is contained in:
parent
99b15f20c0
commit
1828e94fae
4 changed files with 230 additions and 100 deletions
|
|
@ -1,15 +1,19 @@
|
|||
package com.github.damontecres.wholphin.ui.cards
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -24,6 +28,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -41,6 +46,7 @@ import com.github.damontecres.wholphin.ui.AspectRatios
|
|||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
/**
|
||||
|
|
@ -184,3 +190,82 @@ fun BannerCard(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BannerCardWithTitle(
|
||||
title: String?,
|
||||
subtitle: String?,
|
||||
item: BaseItem?,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
cornerText: String? = null,
|
||||
played: Boolean = false,
|
||||
favorite: Boolean = false,
|
||||
playPercent: Double = 0.0,
|
||||
cardHeight: Dp = 120.dp,
|
||||
aspectRatio: Float = AspectRatios.WIDE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
imageType: ImageType = ImageType.PRIMARY,
|
||||
imageContentScale: ContentScale = ContentScale.FillBounds,
|
||||
) {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
|
||||
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
|
||||
val focusedAfterDelay by rememberFocusedAfterDelay(interactionSource)
|
||||
val aspectRationToUse = aspectRatio.coerceAtLeast(AspectRatios.MIN)
|
||||
val width = cardHeight * aspectRationToUse
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(spaceBetween),
|
||||
modifier = modifier.width(width),
|
||||
) {
|
||||
BannerCard(
|
||||
name = null,
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = Modifier,
|
||||
cornerText = cornerText,
|
||||
played = played,
|
||||
favorite = favorite,
|
||||
playPercent = playPercent,
|
||||
cardHeight = cardHeight,
|
||||
aspectRatio = aspectRatio,
|
||||
interactionSource = interactionSource,
|
||||
imageType = imageType,
|
||||
imageContentScale = imageContentScale,
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(bottom = spaceBelow)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = title ?: "",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp)
|
||||
.enableMarquee(focusedAfterDelay),
|
||||
)
|
||||
Text(
|
||||
text = subtitle ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Normal,
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp)
|
||||
.enableMarquee(focusedAfterDelay),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
|
@ -126,21 +125,7 @@ fun SeasonCard(
|
|||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
|
||||
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
|
||||
var focusedAfterDelay by remember { mutableStateOf(false) }
|
||||
|
||||
val hideOverlayDelay = 500L
|
||||
if (focused) {
|
||||
LaunchedEffect(Unit) {
|
||||
delay(hideOverlayDelay)
|
||||
if (focused) {
|
||||
focusedAfterDelay = true
|
||||
} else {
|
||||
focusedAfterDelay = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
focusedAfterDelay = false
|
||||
}
|
||||
val focusedAfterDelay by rememberFocusedAfterDelay(interactionSource)
|
||||
val aspectRationToUse = aspectRatio.coerceAtLeast(AspectRatios.MIN)
|
||||
val width = imageHeight * aspectRationToUse
|
||||
val height = imageWidth * (1f / aspectRationToUse)
|
||||
|
|
@ -212,3 +197,22 @@ fun SeasonCard(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [androidx.compose.runtime.State] which represents if the item has been focused for a while
|
||||
*/
|
||||
@Composable
|
||||
fun rememberFocusedAfterDelay(interactionSource: MutableInteractionSource): androidx.compose.runtime.State<Boolean> {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val state = remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(focused) {
|
||||
if (!focused) {
|
||||
state.value = false
|
||||
return@LaunchedEffect
|
||||
}
|
||||
delay(500L)
|
||||
state.value = true
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,9 +46,11 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||
import com.github.damontecres.wholphin.ui.cards.BannerCardWithTitle
|
||||
import com.github.damontecres.wholphin.ui.cards.GenreCard
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||
|
|
@ -330,91 +332,41 @@ fun HomePageContent(
|
|||
.animateItem(),
|
||||
horizontalPadding = viewOptions.spacing.dp,
|
||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
val mod =
|
||||
cardModifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
position = RowColumn(rowIndex, index)
|
||||
}
|
||||
if (it.isFocused && onFocusPosition != null) {
|
||||
val nonEmptyRowBefore =
|
||||
homeRows
|
||||
.subList(0, rowIndex)
|
||||
.count {
|
||||
it is HomeRowLoadingState.Success && it.items.isEmpty()
|
||||
}
|
||||
onFocusPosition.invoke(
|
||||
RowColumn(
|
||||
rowIndex - nonEmptyRowBefore,
|
||||
index,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.onKeyEvent {
|
||||
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
||||
Timber.v("Clicked play on ${item.id}")
|
||||
onClickPlay.invoke(position, item)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
return@onKeyEvent false
|
||||
}
|
||||
when (item?.type) {
|
||||
BaseItemKind.GENRE -> {
|
||||
GenreCard(
|
||||
genreId = item.id,
|
||||
name = item.name,
|
||||
imageUrl = item.imageUrlOverride,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = mod.height(viewOptions.heightDp.dp),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val imageType =
|
||||
remember(item, viewOptions) {
|
||||
if (item?.type == BaseItemKind.EPISODE) {
|
||||
viewOptions.episodeImageType.imageType
|
||||
} else {
|
||||
viewOptions.imageType.imageType
|
||||
HomePageCardContent(
|
||||
index = index,
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
viewOptions = viewOptions,
|
||||
modifier =
|
||||
cardModifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
position = RowColumn(rowIndex, index)
|
||||
}
|
||||
}
|
||||
val ratio =
|
||||
remember(item, viewOptions) {
|
||||
if (item?.type == BaseItemKind.EPISODE) {
|
||||
viewOptions.episodeAspectRatio.ratio
|
||||
} else {
|
||||
viewOptions.aspectRatio.ratio
|
||||
if (it.isFocused && onFocusPosition != null) {
|
||||
val nonEmptyRowBefore =
|
||||
homeRows
|
||||
.subList(0, rowIndex)
|
||||
.count {
|
||||
it is HomeRowLoadingState.Success && it.items.isEmpty()
|
||||
}
|
||||
onFocusPosition.invoke(
|
||||
RowColumn(
|
||||
rowIndex - nonEmptyRowBefore,
|
||||
index,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val scale =
|
||||
remember(item, viewOptions) {
|
||||
if (item?.type == BaseItemKind.EPISODE) {
|
||||
viewOptions.episodeContentScale.scale
|
||||
} else {
|
||||
viewOptions.contentScale.scale
|
||||
}.onKeyEvent {
|
||||
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
||||
Timber.v("Clicked play on ${item.id}")
|
||||
onClickPlay.invoke(position, item)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
}
|
||||
BannerCard(
|
||||
name = item?.data?.seriesName ?: item?.name,
|
||||
item = item,
|
||||
aspectRatio = ratio,
|
||||
imageType = imageType,
|
||||
imageContentScale = scale,
|
||||
cornerText = item?.ui?.episdodeUnplayedCornerText,
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
favorite = item?.favorite ?: false,
|
||||
playPercent =
|
||||
item?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = mod,
|
||||
interactionSource = null,
|
||||
cardHeight = viewOptions.heightDp.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
return@onKeyEvent false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -516,3 +468,92 @@ fun HomePageHeader(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomePageCardContent(
|
||||
index: Int,
|
||||
item: BaseItem?,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
viewOptions: HomeRowViewOptions,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
when (item?.type) {
|
||||
BaseItemKind.GENRE -> {
|
||||
GenreCard(
|
||||
genreId = item.id,
|
||||
name = item.name,
|
||||
imageUrl = item.imageUrlOverride,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier.height(viewOptions.heightDp.dp),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val imageType =
|
||||
remember(item, viewOptions) {
|
||||
if (item?.type == BaseItemKind.EPISODE) {
|
||||
viewOptions.episodeImageType.imageType
|
||||
} else {
|
||||
viewOptions.imageType.imageType
|
||||
}
|
||||
}
|
||||
val ratio =
|
||||
remember(item, viewOptions) {
|
||||
if (item?.type == BaseItemKind.EPISODE) {
|
||||
viewOptions.episodeAspectRatio.ratio
|
||||
} else {
|
||||
viewOptions.aspectRatio.ratio
|
||||
}
|
||||
}
|
||||
val scale =
|
||||
remember(item, viewOptions) {
|
||||
if (item?.type == BaseItemKind.EPISODE) {
|
||||
viewOptions.episodeContentScale.scale
|
||||
} else {
|
||||
viewOptions.contentScale.scale
|
||||
}
|
||||
}
|
||||
if (viewOptions.showTitles) {
|
||||
BannerCardWithTitle(
|
||||
title = item?.title,
|
||||
subtitle = item?.subtitle,
|
||||
item = item,
|
||||
aspectRatio = ratio,
|
||||
imageType = imageType,
|
||||
imageContentScale = scale,
|
||||
cornerText = item?.ui?.episdodeUnplayedCornerText,
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
favorite = item?.favorite ?: false,
|
||||
playPercent =
|
||||
item?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier,
|
||||
cardHeight = viewOptions.heightDp.dp,
|
||||
)
|
||||
} else {
|
||||
BannerCard(
|
||||
name = item?.data?.seriesName ?: item?.name,
|
||||
item = item,
|
||||
aspectRatio = ratio,
|
||||
imageType = imageType,
|
||||
imageContentScale = scale,
|
||||
cornerText = item?.ui?.episdodeUnplayedCornerText,
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
favorite = item?.favorite ?: false,
|
||||
playPercent =
|
||||
item?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier,
|
||||
interactionSource = null,
|
||||
cardHeight = viewOptions.heightDp.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,11 +238,10 @@ internal object Options {
|
|||
listOf(
|
||||
ViewOptionsCardHeight,
|
||||
ViewOptionsSpacing,
|
||||
ViewOptionsShowTitles,
|
||||
ViewOptionsImageType,
|
||||
ViewOptionsAspectRatio,
|
||||
ViewOptionsContentScale,
|
||||
// TODO
|
||||
// ViewOptionsShowTitles,
|
||||
ViewOptionsUseSeries,
|
||||
),
|
||||
),
|
||||
|
|
@ -265,6 +264,7 @@ internal object Options {
|
|||
listOf(
|
||||
ViewOptionsCardHeight,
|
||||
ViewOptionsSpacing,
|
||||
ViewOptionsShowTitles,
|
||||
ViewOptionsImageType,
|
||||
ViewOptionsAspectRatio,
|
||||
ViewOptionsContentScale,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue