implement episode details screen and ViewModel

- Create `EpisodeScreen` and `EpisodeCard` components with adaptive layouts for wide and narrow viewports.
- Implement `EpisodeScreenViewModel` to fetch "Next Up" episode data using the Jellyfin API.
- Add `EpisodeUiModel` and `CastMember` data classes for content representation.
- Develop custom UI components including `EpisodeHero`, `PlaybackSettings`, `CastRow`, and `EpisodeTopBar`.
- Define a dedicated color palette for the episode view in `EpisodeColors.kt`.
- Integrate navigation to the player activity from the play buttons.
This commit is contained in:
2026-01-18 14:24:47 +01:00
parent f7ce63e50c
commit ae02536ac6
10 changed files with 770 additions and 5 deletions

View File

@@ -0,0 +1,119 @@
package hu.bbara.purefin.app.content.episode
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import org.jellyfin.sdk.model.UUID
@Composable
fun EpisodeCard(
seriesId: String,
modifier: Modifier = Modifier,
viewModel: EpisodeScreenViewModel = hiltViewModel()
) {
LaunchedEffect(seriesId) {
viewModel.selectNextUpEpisodeForSeries(UUID.fromString(seriesId))
}
val episodeItem = viewModel.episode.collectAsState()
if (episodeItem.value != null) {
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(EpisodeBackgroundDark)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp
Box(modifier = Modifier.fillMaxSize()) {
if (isWide) {
Row(modifier = Modifier.fillMaxSize()) {
EpisodeHero(
episode = episodeItem.value!!,
height = 300.dp,
isWide = true,
modifier = Modifier
.fillMaxHeight()
.weight(0.5f)
)
EpisodeDetails(
episode = episodeItem.value!!,
modifier = Modifier
.weight(0.5f)
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(start = contentPadding, end = contentPadding, top = 96.dp, bottom = 32.dp)
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
EpisodeHero(
episode = episodeItem.value!!,
height = 400.dp,
isWide = false,
modifier = Modifier.fillMaxWidth()
)
EpisodeDetails(
episode = episodeItem.value!!,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
.offset(y = (-48).dp)
.padding(bottom = 96.dp)
)
}
}
EpisodeTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
)
if (!isWide) {
FloatingPlayButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(20.dp)
)
}
}
}
} else {
Box(
modifier = modifier
.fillMaxSize()
.background(EpisodeBackgroundDark),
contentAlignment = Alignment.Center
) {
Text(
text = "Loading...",
color = Color.White
)
}
}
}

View File

@@ -0,0 +1,10 @@
package hu.bbara.purefin.app.content.episode
import androidx.compose.ui.graphics.Color
internal val EpisodePrimary = Color(0xFFDDA73C)
internal val EpisodeBackgroundDark = Color(0xFF141414)
internal val EpisodeSurfaceDark = Color(0xFF1F1F1F)
internal val EpisodeSurfaceBorder = Color(0x1AFFFFFF)
internal val EpisodeMuted = Color(0x99FFFFFF)
internal val EpisodeMutedStrong = Color(0x66FFFFFF)

View File

@@ -0,0 +1,466 @@
package hu.bbara.purefin.app.content.episode
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
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.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Cast
import androidx.compose.material.icons.outlined.ClosedCaption
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material.icons.outlined.VolumeUp
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import coil3.compose.AsyncImage
import hu.bbara.purefin.player.PlayerActivity
@Composable
internal fun EpisodeTopBar(modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
GhostIconButton(icon = Icons.Outlined.ArrowBack, contentDescription = "Back")
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast")
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More")
}
}
}
@Composable
private fun GhostIconButton(
icon: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.size(40.dp)
.clip(CircleShape)
.background(EpisodeBackgroundDark.copy(alpha = 0.4f))
.clickable { },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = Color.White
)
}
}
@Composable
internal fun EpisodeHero(
episode: EpisodeUiModel,
height: Dp,
isWide: Boolean,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.height(height)
.background(EpisodeBackgroundDark)
) {
AsyncImage(
model = episode.heroImageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
EpisodeBackgroundDark.copy(alpha = 0.4f),
EpisodeBackgroundDark
)
)
)
)
if (isWide) {
Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.horizontalGradient(
colors = listOf(
Color.Transparent,
EpisodeBackgroundDark.copy(alpha = 0.8f)
)
)
)
)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
PlayButton(size = if (isWide) 96.dp else 80.dp)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun EpisodeDetails(
episode: EpisodeUiModel,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(
text = episode.title,
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 38.sp
)
Spacer(modifier = Modifier.height(16.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MetaChip(text = episode.releaseDate)
MetaChip(text = episode.rating)
MetaChip(text = episode.runtime)
MetaChip(
text = episode.format,
background = EpisodePrimary.copy(alpha = 0.2f),
border = EpisodePrimary.copy(alpha = 0.3f),
textColor = EpisodePrimary
)
}
Spacer(modifier = Modifier.height(24.dp))
PlaybackSettings(episode = episode)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = episode.synopsis,
color = EpisodeMuted,
fontSize = 15.sp,
lineHeight = 22.sp
)
Spacer(modifier = Modifier.height(24.dp))
ActionButtons()
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "Cast",
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
CastRow(cast = episode.cast)
}
}
@Composable
private fun MetaChip(
text: String,
background: Color = Color.White.copy(alpha = 0.1f),
border: Color = Color.Transparent,
textColor: Color = Color.White
) {
Box(
modifier = Modifier
.height(28.dp)
.wrapContentHeight(Alignment.CenterVertically)
.clip(RoundedCornerShape(6.dp))
.background(background)
.border(width = 1.dp, color = border, shape = RoundedCornerShape(6.dp))
.padding(horizontal = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = textColor,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
}
@Composable
private fun PlaybackSettings(episode: EpisodeUiModel) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(EpisodeSurfaceDark)
.border(1.dp, EpisodeSurfaceBorder, RoundedCornerShape(16.dp))
.padding(20.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Tune,
contentDescription = null,
tint = EpisodePrimary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Playback Settings",
color = EpisodeMuted,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
)
}
Spacer(modifier = Modifier.height(16.dp))
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
SettingDropdown(
label = "Audio Track",
icon = Icons.Outlined.VolumeUp,
value = episode.audioTrack
)
SettingDropdown(
label = "Subtitles",
icon = Icons.Outlined.ClosedCaption,
value = episode.subtitles
)
}
}
}
@Composable
private fun SettingDropdown(
label: String,
icon: ImageVector,
value: String
) {
Column {
Text(
text = label,
color = EpisodeMutedStrong,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 4.dp, bottom = 6.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(EpisodeBackgroundDark)
.border(1.dp, EpisodeSurfaceBorder, RoundedCornerShape(12.dp))
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = icon, contentDescription = null, tint = EpisodeMutedStrong)
Spacer(modifier = Modifier.width(10.dp))
Text(text = value, color = Color.White, fontSize = 14.sp)
}
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = EpisodeMutedStrong)
}
}
}
@Composable
private fun ActionButtons() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
ActionButton(
text = "Watchlist",
icon = Icons.Outlined.Add,
modifier = Modifier.weight(1f)
)
ActionButton(
text = "Download",
icon = Icons.Outlined.Download,
modifier = Modifier.weight(1f)
)
}
}
@Composable
private fun ActionButton(
text: String,
icon: ImageVector,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.height(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.White.copy(alpha = 0.05f))
.border(1.dp, EpisodeSurfaceBorder, RoundedCornerShape(12.dp))
.clickable { },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(imageVector = icon, contentDescription = null, tint = Color.White)
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Bold)
}
}
@Composable
private fun CastRow(cast: List<CastMember>) {
LazyRow(
contentPadding = PaddingValues(horizontal = 4.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(cast) { member ->
Column(modifier = Modifier.width(96.dp)) {
Box(
modifier = Modifier
.aspectRatio(4f / 5f)
.clip(RoundedCornerShape(12.dp))
.background(EpisodeSurfaceDark)
) {
if (member.imageUrl == null) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.05f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Outlined.Person,
contentDescription = null,
tint = EpisodeMutedStrong
)
}
} else {
AsyncImage(
model = member.imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
Spacer(modifier = Modifier.height(6.dp))
Text(
text = member.name,
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = member.role,
color = EpisodeMutedStrong,
fontSize = 10.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
@Composable
private fun PlayButton(
size: Dp,
modifier: Modifier = Modifier,
viewModel: EpisodeScreenViewModel = hiltViewModel()
) {
val context = LocalContext.current
val episodeItem = viewModel.episode.collectAsState()
Box(
modifier = modifier
.size(size)
.shadow(24.dp, CircleShape)
.clip(CircleShape)
.background(EpisodePrimary)
.clickable {
val intent = Intent(context, PlayerActivity::class.java)
intent.putExtra("MEDIA_ID", episodeItem.value!!.id.toString())
context.startActivity(intent)
},
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = EpisodeBackgroundDark,
modifier = Modifier.size(42.dp)
)
}
}
@Composable
internal fun FloatingPlayButton(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(56.dp)
.shadow(20.dp, CircleShape)
.clip(CircleShape)
.background(EpisodePrimary),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = EpisodeBackgroundDark
)
}
}

View File

@@ -0,0 +1,23 @@
package hu.bbara.purefin.app.content.episode
import org.jellyfin.sdk.model.UUID
data class CastMember(
val name: String,
val role: String,
val imageUrl: String?
)
data class EpisodeUiModel(
val id: UUID,
val title: String,
val releaseDate: String,
val rating: String,
val runtime: String,
val format: String,
val synopsis: String,
val heroImageUrl: String,
val audioTrack: String,
val subtitles: String,
val cast: List<CastMember>
)

View File

@@ -0,0 +1,22 @@
package hu.bbara.purefin.app.content.episode
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun EpisodeScreen(
seriesId: String,
modifier: Modifier = Modifier
) {
EpisodeCard(
seriesId = seriesId,
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
private fun EpisodeScreenPreview() {
EpisodeScreen(seriesId = "test")
}

View File

@@ -0,0 +1,114 @@
package hu.bbara.purefin.app.content.episode
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.client.JellyfinApiClient
import hu.bbara.purefin.image.JellyfinImageHelper
import hu.bbara.purefin.session.UserSessionRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.ImageType
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltViewModel
class EpisodeScreenViewModel @Inject constructor(
private val jellyfinApiClient: JellyfinApiClient,
private val userSessionRepository: UserSessionRepository
): ViewModel() {
private val _episode = MutableStateFlow<EpisodeUiModel?>(null)
val episode = _episode.asStateFlow()
fun selectNextUpEpisodeForSeries(seriesId: UUID) {
viewModelScope.launch {
val episode = jellyfinApiClient.getNextUpEpisode(seriesId)
if (episode == null) {
_episode.value = null
return@launch
}
selectEpisodeInternal(episode.id)
}
}
fun selectEpisode(episodeId: UUID) {
viewModelScope.launch {
selectEpisodeInternal(episodeId)
}
}
private suspend fun selectEpisodeInternal(episodeId: UUID) {
val episodeInfo = jellyfinApiClient.getItemInfo(episodeId)
val serverUrl = userSessionRepository.serverUrl.first().trim().ifBlank {
"https://jellyfin.bbara.hu"
}
_episode.value = episodeInfo!!.toUiModel(serverUrl)
}
private fun BaseItemDto.toUiModel(serverUrl: String): EpisodeUiModel {
val releaseDate = formatReleaseDate(premiereDate, productionYear)
val rating = officialRating ?: "NR"
val runtime = formatRuntime(runTimeTicks)
val format = container?.uppercase() ?: "VIDEO"
val synopsis = overview ?: "No synopsis available."
val heroImageUrl = id?.let { itemId ->
JellyfinImageHelper.toImageUrl(
url = serverUrl,
itemId = itemId,
type = ImageType.PRIMARY
)
} ?: ""
val cast = people.orEmpty().map { it.toCastMember() }
return EpisodeUiModel(
id = id,
title = name ?: "Unknown title",
releaseDate = releaseDate,
rating = rating,
runtime = runtime,
format = format,
synopsis = synopsis,
heroImageUrl = heroImageUrl,
audioTrack = "Default",
subtitles = "Unknown",
cast = cast
)
}
private fun BaseItemPerson.toCastMember(): CastMember {
return CastMember(
name = name ?: "Unknown",
role = role ?: "",
imageUrl = null
)
}
private fun formatReleaseDate(date: LocalDateTime?, fallbackYear: Int?): String {
if (date == null) {
return fallbackYear?.toString() ?: ""
}
val formatter = DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.getDefault())
return date.toLocalDate().format(formatter)
}
private fun formatRuntime(ticks: Long?): String {
if (ticks == null || ticks <= 0) return ""
val totalSeconds = ticks / 10_000_000
val hours = TimeUnit.SECONDS.toHours(totalSeconds)
val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
return if (hours > 0) {
"${hours}h ${minutes}m"
} else {
"${minutes}m"
}
}
}

View File

@@ -49,6 +49,10 @@ class HomePageViewModel @Inject constructor(
navigationManager.navigate(Route.Movie(movieId))
}
fun onSeriesSelected(seriesId: String) {
navigationManager.navigate(Route.Episode(seriesId))
}
fun onBack() {
navigationManager.pop()
}
@@ -140,7 +144,6 @@ class HomePageViewModel @Inject constructor(
}
}
fun loadLatestLibraryItems(libraryId: UUID) {
if (_libraryItems.value.containsKey(libraryId)) return
viewModelScope.launch {
@@ -150,18 +153,18 @@ class HomePageViewModel @Inject constructor(
BaseItemKind.MOVIE -> PosterItem(
id = it.id,
title = it.name ?: "Unknown",
type = it.type
type = BaseItemKind.MOVIE
)
BaseItemKind.EPISODE -> PosterItem(
id = it.seriesId!!,
title = it.seriesName ?: "Unknown",
type = it.type
type = BaseItemKind.SERIES
)
BaseItemKind.SEASON -> PosterItem(
id = it.seriesId!!,
title = it.seriesName ?: "Unknown",
type = it.type
type = BaseItemKind.SERIES
)
else -> null
}

View File

@@ -193,6 +193,7 @@ fun PosterCard(
{
when (posterItem.type) {
BaseItemKind.MOVIE -> viewModel.onMovieSelected(posterItem.id.toString())
BaseItemKind.SERIES -> viewModel.onSeriesSelected(posterItem.id.toString())
else -> {}
}

View File

@@ -9,4 +9,7 @@ sealed interface Route : NavKey {
@Serializable
data class Movie(val movieId: String) : Route
@Serializable
data class Episode(val seriesId: String) : Route
}

View File

@@ -1,6 +1,7 @@
package hu.bbara.purefin.navigation
import androidx.navigation3.runtime.EntryProviderScope
import hu.bbara.purefin.app.content.episode.EpisodeScreen
import hu.bbara.purefin.app.content.movie.MovieScreen
import hu.bbara.purefin.app.home.HomePage
@@ -11,4 +12,7 @@ fun EntryProviderScope<Route>.appRouteEntryBuilder() {
entry<Route.Movie> {
MovieScreen(movieId = it.movieId)
}
}
entry<Route.Episode> {
EpisodeScreen(seriesId = it.seriesId)
}
}