Several UI fixes (#1730)

## Description
- Fixes not scrolling to the last season on series overview if there is
a Specials season
- Apply the "bouncing" fix from #1543 to all applicable card types
- Fix card images not automatically reloading when changing image type
on grid pages
- Get rid of the sticky headers which overlap items on view options
dialogs and use a fixed header as elsewhere in the app

### Related issues
Related to #1708 which didn't introduce the bug, but made it much more
likely to occur

### Testing
Emulator & unit tests

## Screenshots
N/A, the sticky header UI changes are very minor

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-07-21 17:20:59 -04:00 • committed by GitHub
parent 2aae7eed38
commit 0a4ab7a547
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 159 additions and 130 deletions

View file

@ -1,6 +1,5 @@
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.interaction.collectIsFocusedAsState
@ -219,13 +218,11 @@ fun BannerCardWithTitle(
useSeriesForPrimary: Boolean = item?.useSeriesForPrimary ?: true, useSeriesForPrimary: Boolean = item?.useSeriesForPrimary ?: true,
) { ) {
val focused by interactionSource.collectIsFocusedAsState() val focused by interactionSource.collectIsFocusedAsState()
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
val spaceBelow by animateDpAsState(if (focused) 0.dp else 8.dp)
val focusedAfterDelay by rememberFocusedAfterDelay(interactionSource) val focusedAfterDelay by rememberFocusedAfterDelay(interactionSource)
val aspectRationToUse = aspectRatio.coerceAtLeast(AspectRatios.MIN) val aspectRationToUse = aspectRatio.coerceAtLeast(AspectRatios.MIN)
val width = cardHeight * aspectRationToUse val width = cardHeight * aspectRationToUse
Column( Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween), verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier.width(width), modifier = modifier.width(width),
) { ) {
BannerCard( BannerCard(
@ -245,13 +242,7 @@ fun BannerCardWithTitle(
imageContentScale = imageContentScale, imageContentScale = imageContentScale,
useSeriesForPrimary = useSeriesForPrimary, useSeriesForPrimary = useSeriesForPrimary,
) )
Column( SlidingCardText(focused) {
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
) {
Text( Text(
text = title ?: "", text = title ?: "",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,

View file

@ -59,8 +59,6 @@ fun DiscoverItemCard(
width: Dp = Cards.height2x3 * AspectRatios.TALL, width: Dp = Cards.height2x3 * AspectRatios.TALL,
) { ) {
val focused by interactionSource.collectIsFocusedAsState() 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) } var focusedAfterDelay by remember { mutableStateOf(false) }
val hideOverlayDelay = 500L val hideOverlayDelay = 500L
@ -78,7 +76,7 @@ fun DiscoverItemCard(
} }
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(spaceBetween), verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier.size(width, Dp.Unspecified), modifier = modifier.size(width, Dp.Unspecified),
) { ) {
Card( Card(
@ -180,13 +178,7 @@ fun DiscoverItemCard(
} }
} }
} }
Column( SlidingCardText(focused) {
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
) {
Text( Text(
text = item?.title ?: "", text = item?.title ?: "",
maxLines = 1, maxLines = 1,

View file

@ -51,8 +51,6 @@ fun EpisodeCard(
) { ) {
val dto = item?.data val dto = item?.data
val focused by interactionSource.collectIsFocusedAsState() 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) } var focusedAfterDelay by remember { mutableStateOf(false) }
val hideOverlayDelay = 500L val hideOverlayDelay = 500L
@ -74,7 +72,7 @@ fun EpisodeCard(
val density = LocalDensity.current val density = LocalDensity.current
val imageWidthPx = remember(imageWidth) { with(density) { imageWidth.roundToPx() } } val imageWidthPx = remember(imageWidth) { with(density) { imageWidth.roundToPx() } }
Column( Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween), verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier.size(width, height), modifier = modifier.size(width, height),
) { ) {
Card( Card(
@ -126,13 +124,7 @@ fun EpisodeCard(
} }
} }
} }
Column( SlidingCardText(focused) {
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
) {
Text( Text(
text = dto?.seriesName ?: "", text = dto?.seriesName ?: "",
maxLines = 1, maxLines = 1,

View file

@ -52,8 +52,6 @@ fun GridCard(
) { ) {
val dto = item?.data val dto = item?.data
val focused by interactionSource.collectIsFocusedAsState() 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) } var focusedAfterDelay by remember { mutableStateOf(false) }
val hideOverlayDelay = 500L val hideOverlayDelay = 500L
@ -71,7 +69,7 @@ fun GridCard(
} }
Column( Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween), verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier, modifier = modifier,
) { ) {
Card( Card(
@ -107,13 +105,7 @@ fun GridCard(
) )
} }
AnimatedVisibility(showTitle) { AnimatedVisibility(showTitle) {
Column( SlidingCardText(focused) {
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
) {
Text( Text(
text = item?.title ?: "", text = item?.title ?: "",
maxLines = 1, maxLines = 1,

View file

@ -67,7 +67,7 @@ fun ItemCardImage(
) { ) {
val imageUrlService = LocalImageUrlService.current val imageUrlService = LocalImageUrlService.current
val imageUrl = val imageUrl =
remember(item) { remember(item, imageType, fillWidth, fillHeight) {
item?.let { item?.let {
imageUrlService.getItemImageUrl( imageUrlService.getItemImageUrl(
item, item,

View file

@ -1,6 +1,5 @@
package com.github.damontecres.wholphin.ui.cards package com.github.damontecres.wholphin.ui.cards
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@ -11,7 +10,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
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.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -26,11 +24,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.layout
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.tv.material3.Border import androidx.tv.material3.Border
@ -99,10 +95,6 @@ fun PersonCard(
focusedAfterDelay = false focusedAfterDelay = false
} }
// Do not use `by` here, this way we are Defer reads and recompositions to only when modifier calculates
val spaceBetweenState = animateDpAsState(if (focused) 12.dp else 4.dp, label = "spaceBetween")
val spaceBelowState = animateDpAsState(if (focused) 4.dp else 12.dp, label = "spaceBelow")
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp), // Fixed base spacing verticalArrangement = Arrangement.spacedBy(4.dp), // Fixed base spacing
modifier = modifier, modifier = modifier,
@ -170,21 +162,7 @@ fun PersonCard(
.clip(CircleShape), .clip(CircleShape),
) )
} }
Column( SlidingCardText(focused) {
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
// Optimization: move animation reads to layout/draw phase
.offset {
IntOffset(0, (spaceBetweenState.value - 4.dp).roundToPx())
}.layout { measurable, constraints ->
val paddingPx = spaceBelowState.value.roundToPx()
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height + paddingPx) {
placeable.placeRelative(0, 0)
}
}.fillMaxWidth(),
) {
Text( Text(
text = name ?: "", text = name ?: "",
maxLines = 1, maxLines = 1,

View file

@ -0,0 +1,46 @@
package com.github.damontecres.wholphin.ui.cards
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Slides the [content] up/down as the card is [focused] or not
*
* This is used for the card's title, moving it down when focused as the card itself scales up
*/
@Composable
fun SlidingCardText(
focused: Boolean,
modifier: Modifier = Modifier,
spaceBetweenFocused: Dp = 4.dp,
spaceBetweenUnfocused: Dp = 12.dp,
content: @Composable ColumnScope.() -> Unit,
) {
val spaceBetween =
animateDpAsState(
if (focused) spaceBetweenFocused else spaceBetweenUnfocused,
label = "spaceBetween",
)
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
modifier
.layout { measurable, constraints ->
val topPaddingPx = (spaceBetweenUnfocused - spaceBetween.value).roundToPx()
val bottomPaddingPx = spaceBetween.value.roundToPx()
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height + bottomPaddingPx + topPaddingPx) {
placeable.placeRelative(0, topPaddingPx)
}
}.fillMaxWidth(),
content = content,
)
}

View file

@ -53,8 +53,6 @@ fun ViewMoreCard(
showTitle: Boolean = true, showTitle: Boolean = true,
) { ) {
val focused by interactionSource.collectIsFocusedAsState() 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) } var focusedAfterDelay by remember { mutableStateOf(false) }
val hideOverlayDelay = 500L val hideOverlayDelay = 500L
@ -79,7 +77,7 @@ fun ViewMoreCard(
size.height.takeIf { it.isSpecified } ?: (size.height * (1f / aspectRatio.ratio)) size.height.takeIf { it.isSpecified } ?: (size.height * (1f / aspectRatio.ratio))
} }
Column( Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween), verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier, modifier = modifier,
) { ) {
Card( Card(
@ -108,14 +106,7 @@ fun ViewMoreCard(
} }
} }
if (showTitle) { if (showTitle) {
Column( SlidingCardText(focused) {
verticalArrangement = Arrangement.spacedBy(0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier
.width(width)
.padding(bottom = spaceBelow),
) {
Text( Text(
text = stringResource(R.string.view_more), text = stringResource(R.string.view_more),
maxLines = 1, maxLines = 1,

View file

@ -4,8 +4,11 @@ import android.view.Gravity
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.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -19,6 +22,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@ -74,45 +78,52 @@ fun ViewOptionsDialog(
ViewOptionsType.DENSE_LIST -> ViewOptions.LIST_OPTIONS ViewOptionsType.DENSE_LIST -> ViewOptions.LIST_OPTIONS
} }
} }
LazyColumn( Column(
state = columnState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(0.dp), verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = modifier =
Modifier Modifier
.width(256.dp) .width(256.dp)
.heightIn(max = 380.dp) .heightIn(max = 380.dp)
.focusRequester(focusRequester)
.background( .background(
MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp), MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
), ),
) { ) {
stickyHeader { Text(
Text( text = stringResource(R.string.view_options),
text = stringResource(R.string.view_options), style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center,
) modifier =
} Modifier
items(options, key = { it.title }) { pref -> .padding(vertical = 8.dp)
pref as AppPreference<ViewOptions, Any> .fillMaxWidth(),
val interactionSource = remember { MutableInteractionSource() } )
val value = pref.getter.invoke(viewOptions) LazyColumn(
ComposablePreference( state = columnState,
preference = pref, contentPadding = PaddingValues(16.dp),
value = value, verticalArrangement = Arrangement.spacedBy(0.dp),
onNavigate = {}, modifier = Modifier.focusRequester(focusRequester),
onValueChange = { newValue -> ) {
onViewOptionsChange.invoke(pref.setter(viewOptions, newValue)) items(options, key = { it.title }) { pref ->
}, pref as AppPreference<ViewOptions, Any>
interactionSource = interactionSource, val interactionSource = remember { MutableInteractionSource() }
modifier = Modifier, val value = pref.getter.invoke(viewOptions)
onClickPreference = { pref -> ComposablePreference(
if (pref == ViewOptions.ViewOptionsReset) { preference = pref,
onViewOptionsChange.invoke(defaultViewOptions) value = value,
} onNavigate = {},
}, onValueChange = { newValue ->
) onViewOptionsChange.invoke(pref.setter(viewOptions, newValue))
},
interactionSource = interactionSource,
modifier = Modifier,
onClickPreference = { pref ->
if (pref == ViewOptions.ViewOptionsReset) {
onViewOptionsChange.invoke(defaultViewOptions)
}
},
)
}
} }
} }
} }

View file

@ -4,8 +4,11 @@ import android.view.Gravity
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.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -19,6 +22,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@ -54,40 +58,47 @@ fun LiveTvViewOptionsDialog(
window.setGravity(Gravity.END) window.setGravity(Gravity.END)
window.setDimAmount(0f) window.setDimAmount(0f)
} }
LazyColumn( Column(
state = columnState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(0.dp), verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = modifier =
Modifier Modifier
.width(256.dp) .width(256.dp)
.heightIn(max = 380.dp) .heightIn(max = 380.dp)
.focusRequester(focusRequester)
.background( .background(
MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp), MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
), ),
) { ) {
stickyHeader { Text(
Text( text = stringResource(R.string.view_options),
text = stringResource(R.string.view_options), style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center,
) modifier =
} Modifier
items(liveTvPreferences) { pref -> .padding(vertical = 8.dp)
pref as AppPreference<AppPreferences, Any> .fillMaxWidth(),
val interactionSource = remember { MutableInteractionSource() } )
val value = pref.getter.invoke(preferences) LazyColumn(
ComposablePreference( state = columnState,
preference = pref, contentPadding = PaddingValues(16.dp),
value = value, verticalArrangement = Arrangement.spacedBy(0.dp),
onNavigate = {}, modifier = Modifier.focusRequester(focusRequester),
onValueChange = { newValue -> ) {
onViewOptionsChange.invoke(pref.setter(preferences, newValue)) items(liveTvPreferences) { pref ->
}, pref as AppPreference<AppPreferences, Any>
interactionSource = interactionSource, val interactionSource = remember { MutableInteractionSource() }
modifier = Modifier, val value = pref.getter.invoke(preferences)
) ComposablePreference(
preference = pref,
value = value,
onNavigate = {},
onValueChange = { newValue ->
onViewOptionsChange.invoke(pref.setter(preferences, newValue))
},
interactionSource = interactionSource,
modifier = Modifier,
)
}
} }
} }
} }

View file

@ -746,7 +746,7 @@ suspend fun findIndexByNumberOrId(
// Start searching from the target number and choose direction from there // Start searching from the target number and choose direction from there
val num = list.getBlocking(listIndex)?.indexNumber val num = list.getBlocking(listIndex)?.indexNumber
if (num.lt(targetNum)) { if (num.lt(targetNum)) {
for (i in listIndex + 1 until list.lastIndex) { for (i in listIndex + 1 until list.size) {
val item = list.getBlocking(i) val item = list.getBlocking(i)
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) { if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
return i return i

View file

@ -28,6 +28,7 @@ class TestFindIndexByNumberOrId {
val epS02E03 = create(3, 2) val epS02E03 = create(3, 2)
val epS02E04 = create(4, 2) val epS02E04 = create(4, 2)
val epS02E05 = create(5, 2) val epS02E05 = create(5, 2)
val epS02E06 = create(6, 2)
val epS00E08 = create(8, 0) val epS00E08 = create(8, 0)
val epS00E02 = create(2, 0) val epS00E02 = create(2, 0)
@ -70,6 +71,30 @@ class TestFindIndexByNumberOrId {
} }
} }
@Test
fun `Test last index with zero start`() =
runTest {
val episodes =
BlockingList.of(
listOf(
create(0, 2),
epS02E01,
epS02E02,
epS02E03,
epS02E04,
epS02E05,
epS02E06,
),
)
findIndexByNumberOrId(
targetNum = 6,
targetId = epS02E06.id,
list = BlockingList.of(episodes),
).let { index ->
Assert.assertEquals(6, index)
}
}
@Test @Test
fun `Test missing episodes`() = fun `Test missing episodes`() =
runTest { runTest {