mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat(streams): Implement tablet layout for StreamsScreen with dynamic content rendering
This commit is contained in:
parent
ae95c9d004
commit
a92954a762
3 changed files with 483 additions and 45 deletions
|
|
@ -129,6 +129,7 @@ kotlin {
|
|||
commonMain.dependencies {
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network.ktor3)
|
||||
implementation("dev.chrisbanes.haze:haze:1.7.2")
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.material3)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
|
|
@ -122,12 +123,94 @@ fun StreamsScreen(
|
|||
background ?: poster
|
||||
}
|
||||
|
||||
Box(
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
// Background artwork
|
||||
val isTabletLayout = maxWidth >= 768.dp
|
||||
|
||||
if (isTabletLayout) {
|
||||
TabletStreamsLayout(
|
||||
isEpisode = isEpisode,
|
||||
title = title,
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = background,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
uiState = uiState,
|
||||
resumePositionMs = effectiveResumePositionMs,
|
||||
onStreamSelected = onStreamSelected,
|
||||
)
|
||||
} else {
|
||||
MobileStreamsLayout(
|
||||
isEpisode = isEpisode,
|
||||
title = title,
|
||||
logo = logo,
|
||||
heroArtwork = heroArtwork,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
uiState = uiState,
|
||||
resumePositionMs = effectiveResumePositionMs,
|
||||
onStreamSelected = onStreamSelected,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top))
|
||||
.padding(start = 12.dp, top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
NuvioBackButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.45f),
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.background.copy(alpha = 0.45f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickable(onClick = { StreamsRepository.reload(type, videoId) }),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Refresh,
|
||||
contentDescription = "Refresh streams",
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MobileStreamsLayout(
|
||||
isEpisode: Boolean,
|
||||
title: String,
|
||||
logo: String?,
|
||||
heroArtwork: String?,
|
||||
seasonNumber: Int?,
|
||||
episodeNumber: Int?,
|
||||
episodeTitle: String?,
|
||||
uiState: StreamsUiState,
|
||||
resumePositionMs: Long?,
|
||||
onStreamSelected: (StreamItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
if (heroArtwork != null) {
|
||||
AsyncImage(
|
||||
model = heroArtwork,
|
||||
|
|
@ -137,7 +220,6 @@ fun StreamsScreen(
|
|||
.blur(22.dp),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
// Dark scrim
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -147,10 +229,8 @@ fun StreamsScreen(
|
|||
|
||||
val streamBlendColor = MaterialTheme.colorScheme.background
|
||||
|
||||
// Main content column
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Hero block
|
||||
if (isEpisode) {
|
||||
if (isEpisode && seasonNumber != null && episodeNumber != null) {
|
||||
EpisodeHeroBlock(
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
|
|
@ -189,9 +269,9 @@ fun StreamsScreen(
|
|||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
if (effectiveResumePositionMs != null && effectiveResumePositionMs > 0L) {
|
||||
if (resumePositionMs != null && resumePositionMs > 0L) {
|
||||
ResumeBanner(
|
||||
positionMs = effectiveResumePositionMs,
|
||||
positionMs = resumePositionMs,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -209,44 +289,11 @@ fun StreamsScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top))
|
||||
.padding(start = 12.dp, top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
NuvioBackButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.45f),
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.background.copy(alpha = 0.45f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickable(onClick = { StreamsRepository.reload(type, videoId) }),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Refresh,
|
||||
contentDescription = "Refresh streams",
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResumeBanner(
|
||||
internal fun ResumeBanner(
|
||||
positionMs: Long,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -415,7 +462,7 @@ private fun EpisodeHeroBlock(
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
@Composable
|
||||
private fun ProviderFilterRow(
|
||||
internal fun ProviderFilterRow(
|
||||
groups: List<AddonStreamGroup>,
|
||||
selectedFilter: String?,
|
||||
onFilterSelected: (String?) -> Unit,
|
||||
|
|
@ -511,7 +558,7 @@ private fun FilterChip(
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
@Composable
|
||||
private fun StreamList(
|
||||
internal fun StreamList(
|
||||
uiState: StreamsUiState,
|
||||
onStreamSelected: (StreamItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,390 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.isIos
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
|
||||
@Composable
|
||||
internal fun TabletStreamsLayout(
|
||||
isEpisode: Boolean,
|
||||
title: String,
|
||||
logo: String?,
|
||||
poster: String?,
|
||||
background: String?,
|
||||
episodeThumbnail: String?,
|
||||
seasonNumber: Int?,
|
||||
episodeNumber: Int?,
|
||||
episodeTitle: String?,
|
||||
uiState: StreamsUiState,
|
||||
resumePositionMs: Long?,
|
||||
onStreamSelected: (StreamItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hazeState = rememberHazeState()
|
||||
val tabletBackdrop = remember(isEpisode, episodeThumbnail, background, poster) {
|
||||
resolveTabletBackdrop(
|
||||
isEpisode = isEpisode,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
background = background,
|
||||
poster = poster,
|
||||
)
|
||||
}
|
||||
var backdropVisible by remember(tabletBackdrop) { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(tabletBackdrop) {
|
||||
backdropVisible = tabletBackdrop == null
|
||||
if (tabletBackdrop != null) {
|
||||
backdropVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
val backdropAlpha by animateFloatAsState(
|
||||
targetValue = if (backdropVisible) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 520),
|
||||
label = "tablet_backdrop_alpha",
|
||||
)
|
||||
val backdropScale by animateFloatAsState(
|
||||
targetValue = if (backdropVisible) 1f else 1.05f,
|
||||
animationSpec = tween(durationMillis = 620),
|
||||
label = "tablet_backdrop_scale",
|
||||
)
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.hazeSource(state = hazeState),
|
||||
) {
|
||||
if (tabletBackdrop != null) {
|
||||
AsyncImage(
|
||||
model = tabletBackdrop,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
alpha = backdropAlpha
|
||||
scaleX = backdropScale
|
||||
scaleY = backdropScale
|
||||
},
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = 0.2f),
|
||||
Color.Black.copy(alpha = 0.4f),
|
||||
Color.Black.copy(alpha = 0.6f),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(0.4f)
|
||||
.fillMaxHeight()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (isEpisode && seasonNumber != null && episodeNumber != null) {
|
||||
TabletEpisodeInfoPanel(
|
||||
logo = logo,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = episodeTitle,
|
||||
showTitle = title,
|
||||
)
|
||||
} else {
|
||||
TabletMovieInfoPanel(
|
||||
title = title,
|
||||
logo = logo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(0.6f)
|
||||
.fillMaxHeight()
|
||||
.padding(
|
||||
top = if (isIos) 20.dp else 60.dp,
|
||||
end = 12.dp,
|
||||
bottom = 12.dp,
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.hazeEffect(state = hazeState)
|
||||
.background(Color.Black.copy(alpha = 0.22f)),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
if (resumePositionMs != null && resumePositionMs > 0L) {
|
||||
ResumeBanner(
|
||||
positionMs = resumePositionMs,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
ProviderFilterRow(
|
||||
groups = uiState.groups,
|
||||
selectedFilter = uiState.selectedFilter,
|
||||
onFilterSelected = { addonId -> StreamsRepository.selectFilter(addonId) },
|
||||
)
|
||||
|
||||
ActiveScrapersStatusBlock(
|
||||
groups = uiState.groups,
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
)
|
||||
|
||||
StreamList(
|
||||
uiState = uiState,
|
||||
onStreamSelected = onStreamSelected,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabletMovieInfoPanel(
|
||||
title: String,
|
||||
logo: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(0.8f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
if (!logo.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = logo,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
} else if (title.isNotBlank()) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.displayLarge.copy(
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
letterSpacing = (-0.5).sp,
|
||||
shadow = Shadow(
|
||||
color = Color.Black.copy(alpha = 0.8f),
|
||||
offset = Offset(0f, 2f),
|
||||
blurRadius = 4f,
|
||||
),
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "No metadata available",
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontSize = 16.sp,
|
||||
fontStyle = FontStyle.Italic,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabletEpisodeInfoPanel(
|
||||
logo: String?,
|
||||
seasonNumber: Int,
|
||||
episodeNumber: Int,
|
||||
episodeTitle: String?,
|
||||
showTitle: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val textShadow = Shadow(
|
||||
color = Color.Black,
|
||||
offset = Offset(0f, 0f),
|
||||
blurRadius = 4f,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(0.8f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
if (!logo.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = logo,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = showTitle,
|
||||
style = MaterialTheme.typography.displayMedium.copy(
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
letterSpacing = (-0.5).sp,
|
||||
shadow = textShadow,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "S${seasonNumber}E${episodeNumber} - ${episodeTitle?.takeIf { it.isNotBlank() } ?: "Episode"}",
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
shadow = textShadow,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.95f),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveScrapersStatusBlock(
|
||||
groups: List<AddonStreamGroup>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val activeScrapers = remember(groups) {
|
||||
groups.filter { it.isLoading }.map { it.addonName }.distinct()
|
||||
}
|
||||
if (activeScrapers.isEmpty()) return
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Active scrapers",
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
activeScrapers.forEach { addonName ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f))
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
) {
|
||||
Text(
|
||||
text = addonName,
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveTabletBackdrop(
|
||||
isEpisode: Boolean,
|
||||
episodeThumbnail: String?,
|
||||
background: String?,
|
||||
poster: String?,
|
||||
): String? {
|
||||
if (!isEpisode) return background ?: poster
|
||||
|
||||
val preferredEpisodeThumbnail = episodeThumbnail?.takeIf {
|
||||
it.isNotBlank() && it != poster
|
||||
}
|
||||
return preferredEpisodeThumbnail ?: background ?: poster
|
||||
}
|
||||
Loading…
Reference in a new issue