mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +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
|
package com.github.damontecres.wholphin.ui.cards
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
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.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
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.Cards
|
||||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||||
|
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
|
@ -126,21 +125,7 @@ fun SeasonCard(
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
|
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
|
||||||
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
|
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
|
||||||
var focusedAfterDelay by remember { mutableStateOf(false) }
|
val focusedAfterDelay by rememberFocusedAfterDelay(interactionSource)
|
||||||
|
|
||||||
val hideOverlayDelay = 500L
|
|
||||||
if (focused) {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
delay(hideOverlayDelay)
|
|
||||||
if (focused) {
|
|
||||||
focusedAfterDelay = true
|
|
||||||
} else {
|
|
||||||
focusedAfterDelay = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
focusedAfterDelay = false
|
|
||||||
}
|
|
||||||
val aspectRationToUse = aspectRatio.coerceAtLeast(AspectRatios.MIN)
|
val aspectRationToUse = aspectRatio.coerceAtLeast(AspectRatios.MIN)
|
||||||
val width = imageHeight * aspectRationToUse
|
val width = imageHeight * aspectRationToUse
|
||||||
val height = imageWidth * (1f / 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 androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
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.GenreCard
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
|
|
@ -330,7 +332,13 @@ fun HomePageContent(
|
||||||
.animateItem(),
|
.animateItem(),
|
||||||
horizontalPadding = viewOptions.spacing.dp,
|
horizontalPadding = viewOptions.spacing.dp,
|
||||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||||
val mod =
|
HomePageCardContent(
|
||||||
|
index = index,
|
||||||
|
item = item,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
viewOptions = viewOptions,
|
||||||
|
modifier =
|
||||||
cardModifier
|
cardModifier
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
if (it.isFocused) {
|
if (it.isFocused) {
|
||||||
|
|
@ -357,64 +365,8 @@ fun HomePageContent(
|
||||||
return@onKeyEvent true
|
return@onKeyEvent true
|
||||||
}
|
}
|
||||||
return@onKeyEvent false
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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(
|
listOf(
|
||||||
ViewOptionsCardHeight,
|
ViewOptionsCardHeight,
|
||||||
ViewOptionsSpacing,
|
ViewOptionsSpacing,
|
||||||
|
ViewOptionsShowTitles,
|
||||||
ViewOptionsImageType,
|
ViewOptionsImageType,
|
||||||
ViewOptionsAspectRatio,
|
ViewOptionsAspectRatio,
|
||||||
ViewOptionsContentScale,
|
ViewOptionsContentScale,
|
||||||
// TODO
|
|
||||||
// ViewOptionsShowTitles,
|
|
||||||
ViewOptionsUseSeries,
|
ViewOptionsUseSeries,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -265,6 +264,7 @@ internal object Options {
|
||||||
listOf(
|
listOf(
|
||||||
ViewOptionsCardHeight,
|
ViewOptionsCardHeight,
|
||||||
ViewOptionsSpacing,
|
ViewOptionsSpacing,
|
||||||
|
ViewOptionsShowTitles,
|
||||||
ViewOptionsImageType,
|
ViewOptionsImageType,
|
||||||
ViewOptionsAspectRatio,
|
ViewOptionsAspectRatio,
|
||||||
ViewOptionsContentScale,
|
ViewOptionsContentScale,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue