UI updates to movie & search pages

This commit is contained in:
Damontecres 2025-10-09 13:01:57 -04:00
parent fe1d63793d
commit 616ffe8de3
No known key found for this signature in database
11 changed files with 235 additions and 41 deletions

View file

@ -1,6 +1,6 @@
# Dolphin - an OSS Android TV client for Jellyfin # Dolphin - an OSS Android TV client for Jellyfin
This is an Android TV client for [Jellyfin](https://jellyfin.org/). It aims to provide a more "Plex app"-like app experience for users migrating from Plex to Jellyfin. This is an Android TV client for [Jellyfin](https://jellyfin.org/). It aims to provide a more Plex inspired UI/UX experience for users migrating from Plex to Jellyfin.
This is not a fork of the [official client](https://github.com/jellyfin/jellyfin-androidtv). The user interface and controls have been written completely from scratch. This is not a fork of the [official client](https://github.com/jellyfin/jellyfin-androidtv). The user interface and controls have been written completely from scratch.
@ -15,7 +15,7 @@ That said, Dolphin does not yet implement every feature in Jellyfin. It is a wor
- A navigation drawer for quick access to libraries, search, and settings from almost anywhere in the app - A navigation drawer for quick access to libraries, search, and settings from almost anywhere in the app
- Show Movie/TV Show titles when browsing libraries - Show Movie/TV Show titles when browsing libraries
- Play TV Show theme music, if available - Play TV Show theme music, if available
- "Plex app"-like playback controls, plus other enhancements, such as: - Plex inspired playback controls, plus other enhancements, such as:
- Using D-Pad left/right for seeking during playback - Using D-Pad left/right for seeking during playback
- Subtly show playback position while seeking w/ D-Pad - Subtly show playback position while seeking w/ D-Pad
- Quickly access video chapters during playback - Quickly access video chapters during playback
@ -29,8 +29,8 @@ That said, Dolphin does not yet implement every feature in Jellyfin. It is a wor
- https://developer.android.com/distribute/marketing-tools/alternative-distribution#unknown-sources - https://developer.android.com/distribute/marketing-tools/alternative-distribution#unknown-sources
- https://www.aftvnews.com/how-to-enable-apps-from-unknown-sources-on-an-amazon-fire-tv-or-fire-tv-stick/ - https://www.aftvnews.com/how-to-enable-apps-from-unknown-sources-on-an-amazon-fire-tv-or-fire-tv-stick/
1. Install the APK on your Android TV device with one of these options: 1. Install the APK on your Android TV device with one of these options:
- Install a browser program such as [Downloader](https://www.aftvnews.com/downloader/), use it to get the latest apk with short code `TODO ENTER A VALUE HERE` or URL: https://aftv.news/TODOTODO - Install a browser program such as [Downloader](https://www.aftvnews.com/downloader/), use it to get the latest apk with short code `Dolphin ENTER A VALUE HERE` or URL: https://aftv.news/ Dolphin
- Download the latest APK release from the [releases page](https://github.com/damontecres/StashAppAndroidTV/releases/latest) or https://aftv.news/745800 - Download the latest APK release from the [releases page](https://github.com/damontecres/Dolphin/releases/latest) or https://aftv.news/ Dolphin
- Put the APK on an SD Card/USB stick/network share and use a file manager app from the Google Play Store / Amazon AppStore (e.g. `FX File Explorer`). Android's preinstalled file manager probably will not work! - Put the APK on an SD Card/USB stick/network share and use a file manager app from the Google Play Store / Amazon AppStore (e.g. `FX File Explorer`). Android's preinstalled file manager probably will not work!
- Use `Send files to TV` from the Google Play Store on your phone & TV - Use `Send files to TV` from the Google Play Store on your phone & TV
- (Expert) Use [ADB](https://developer.android.com/studio/command-line/adb) to install the APK from your computer ([guide](https://fossbytes.com/side-load-apps-android-tv/#h-how-to-sideload-apps-on-your-android-tv-using-adb)) - (Expert) Use [ADB](https://developer.android.com/studio/command-line/adb) to install the APK from your computer ([guide](https://fossbytes.com/side-load-apps-android-tv/#h-how-to-sideload-apps-on-your-android-tv-using-adb))

View file

@ -7,6 +7,7 @@ import androidx.datastore.core.DataStore
import com.github.damontecres.dolphin.preferences.AppPreferences import com.github.damontecres.dolphin.preferences.AppPreferences
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.api.client.exception.InvalidStatusException
import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.api.client.extensions.userApi
@ -20,6 +21,7 @@ import javax.inject.Singleton
class ServerRepository class ServerRepository
@Inject @Inject
constructor( constructor(
val jellyfin: Jellyfin,
val serverDao: JellyfinServerDao, val serverDao: JellyfinServerDao,
val apiClient: ApiClient, val apiClient: ApiClient,
val userPreferencesDataStore: DataStore<AppPreferences>, val userPreferencesDataStore: DataStore<AppPreferences>,

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.Player import androidx.media3.common.Player
import coil3.request.ErrorResult
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
@ -253,3 +254,14 @@ fun Activity.keepScreenOn(keep: Boolean) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
} }
fun logCoilError(
url: String?,
errorResult: ErrorResult,
) {
if (errorResult.throwable is coil3.network.HttpException) {
Timber.w("Error loading image: %s for %s", errorResult.throwable.localizedMessage, url)
} else {
Timber.e(errorResult.throwable, "Error loading image: %s", url)
}
}

View file

@ -0,0 +1,143 @@
package com.github.damontecres.dolphin.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.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
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.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.AppColors
import com.github.damontecres.dolphin.ui.enableMarquee
import com.github.damontecres.dolphin.util.seasonEpisode
import kotlinx.coroutines.delay
@Composable
fun EpisodeCard(
item: BaseItem?,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
imageHeight: Dp = Dp.Unspecified,
imageWidth: Dp = Dp.Unspecified,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val dto = item?.data
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 aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f)
val width = imageHeight * aspectRatio
val height = imageWidth * (1f / aspectRatio)
Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween),
modifier = modifier.size(width, height),
) {
Card(
modifier =
Modifier
.size(imageWidth, imageHeight)
.aspectRatio(aspectRatio),
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
colors =
CardDefaults.colors(
containerColor = Color.Transparent,
),
) {
Box(
modifier =
Modifier
.fillMaxSize(),
) {
ItemCardImage(
imageUrl = item?.imageUrl,
name = item?.name,
showOverlay = false,
favorite = dto?.userData?.isFavorite ?: false,
watched = dto?.userData?.played ?: false,
unwatchedCount = dto?.userData?.unplayedItemCount ?: -1,
watchedPercent = dto?.userData?.playedPercentage,
useFallbackText = false,
modifier =
Modifier
.fillMaxSize(),
)
Box(
modifier =
Modifier
.align(Alignment.TopEnd)
.background(AppColors.TransparentBlack50, shape = RoundedCornerShape(8.dp)),
) {
Text(
text = dto?.seasonEpisode ?: "",
modifier = Modifier.padding(4.dp),
)
}
}
}
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.padding(bottom = spaceBelow).fillMaxWidth(),
) {
Text(
text = dto?.seriesName ?: "",
maxLines = 1,
textAlign = TextAlign.Center,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.enableMarquee(focusedAfterDelay),
)
Text(
text = item?.name ?: "",
maxLines = 1,
textAlign = TextAlign.Center,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.enableMarquee(focusedAfterDelay),
)
}
}
}

View file

@ -52,10 +52,10 @@ import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.enableMarquee import com.github.damontecres.dolphin.ui.enableMarquee
import com.github.damontecres.dolphin.ui.ifElse import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.logCoilError
import com.github.damontecres.dolphin.util.seasonEpisode import com.github.damontecres.dolphin.util.seasonEpisode
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import timber.log.Timber
@Composable @Composable
fun ItemCard( fun ItemCard(
@ -176,11 +176,8 @@ fun ItemCardImage(
contentDescription = name, contentDescription = name,
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
alignment = Alignment.Center, alignment = Alignment.Center,
// TODO error/fallback images
error = null,
fallback = null,
onError = { onError = {
Timber.e(it.result.throwable, "Error loading image: $imageUrl") logCoilError(imageUrl, it.result)
imageError = true imageError = true
}, },
modifier = modifier =
@ -189,6 +186,7 @@ fun ItemCardImage(
.align(Alignment.TopCenter), .align(Alignment.TopCenter),
) )
} else { } else {
// TODO options for overriding fallback
Box( Box(
modifier = modifier =
Modifier Modifier

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@ -63,10 +62,9 @@ fun ItemRow(
LazyRow( LazyRow(
state = state, state = state,
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
modifier = modifier =
Modifier Modifier
.padding(start = 16.dp)
.fillMaxWidth() .fillMaxWidth()
.focusRestorer(focusPair?.focusRequester ?: firstFocus), .focusRestorer(focusPair?.focusRequester ?: firstFocus),
) { ) {

View file

@ -1,13 +1,13 @@
package com.github.damontecres.dolphin.ui.cards package com.github.damontecres.dolphin.ui.cards
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -17,7 +17,6 @@ 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.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.tv.material3.Card import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults import androidx.tv.material3.CardDefaults
@ -32,8 +31,6 @@ fun PersonCard(
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
cardWidth: Dp = 150.dp * .75f,
cardHeight: Dp = 200.dp * .75f,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
val hideOverlayDelay = 1_000L val hideOverlayDelay = 1_000L
@ -53,34 +50,44 @@ fun PersonCard(
} else { } else {
focusedAfterDelay = false focusedAfterDelay = false
} }
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
Card( val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween),
modifier = modifier, modifier = modifier,
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
colors =
CardDefaults.colors(
containerColor = Color.Transparent,
),
) { ) {
Column( Card(
verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier,
modifier = Modifier.width(cardWidth), onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
colors =
CardDefaults.colors(
containerColor = Color.Transparent,
),
) { ) {
ItemCardImage( ItemCardImage(
imageUrl = item.imageUrl, imageUrl = item.imageUrl,
name = item.name, name = item.name,
showOverlay = true, showOverlay = false,
favorite = false, favorite = false,
watched = false, watched = false,
unwatchedCount = -1, unwatchedCount = -1,
watchedPercent = null, watchedPercent = null,
useFallbackText = false,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(cardHeight), .aspectRatio(2f / 3f), // TODO,
) )
}
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
) {
Text( Text(
text = item.name ?: "", text = item.name ?: "",
maxLines = 1, maxLines = 1,

View file

@ -5,6 +5,7 @@ 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@ -52,7 +53,10 @@ fun PersonRow(
item = item, item = item,
onClick = { onClick.invoke(item) }, onClick = { onClick.invoke(item) },
onLongClick = { onLongClick?.invoke(item) }, onLongClick = { onLongClick?.invoke(item) },
modifier = Modifier.ifElse(index == 0, Modifier.focusRequester(firstFocus)), modifier =
Modifier
.width(120.dp)
.ifElse(index == 0, Modifier.focusRequester(firstFocus)),
) )
} }
} }

View file

@ -289,7 +289,8 @@ fun MovieDetailsContent(
bringIntoViewRequester = bringIntoViewRequester, bringIntoViewRequester = bringIntoViewRequester,
overviewOnClick = overviewOnClick, overviewOnClick = overviewOnClick,
Modifier Modifier
.fillMaxWidth(.7f), .fillMaxWidth(.7f)
.padding(bottom = 8.dp),
) )
ExpandablePlayButtons( ExpandablePlayButtons(
resumePosition = resumePosition, resumePosition = resumePosition,

View file

@ -57,6 +57,7 @@ fun MovieDetailsHeader(
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier, modifier = modifier,
) { ) {
// Title // Title
@ -64,7 +65,7 @@ fun MovieDetailsHeader(
text = movie.name ?: "", text = movie.name ?: "",
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = style =
MaterialTheme.typography.displayMedium.copy( MaterialTheme.typography.displayLarge.copy(
shadow = shadow =
Shadow( Shadow(
color = Color.DarkGray, color = Color.DarkGray,
@ -93,12 +94,13 @@ fun MovieDetailsHeader(
?.let { ?.let {
add(it) add(it)
} }
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
dto.officialRating?.let(::add) dto.officialRating?.let(::add)
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
} }
DotSeparatedRow( DotSeparatedRow(
texts = details, texts = details,
textStyle = MaterialTheme.typography.titleLarge, textStyle = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 8.dp),
) )
} }
dto.communityRating?.let { dto.communityRating?.let {
@ -109,10 +111,10 @@ fun MovieDetailsHeader(
enabled = false, enabled = false,
precision = StarRatingPrecision.HALF, precision = StarRatingPrecision.HALF,
playSoundOnFocus = true, playSoundOnFocus = true,
modifier = Modifier.height(40.dp), modifier = Modifier.height(32.dp),
) )
} else { } else {
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(32.dp))
} }
} }
@ -157,7 +159,7 @@ fun MovieDetailsHeader(
Row( Row(
modifier = modifier =
Modifier Modifier
.padding(top = 8.dp, start = 16.dp) .padding(start = 16.dp)
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {

View file

@ -25,6 +25,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.DefaultItemFields
import com.github.damontecres.dolphin.ui.cards.EpisodeCard
import com.github.damontecres.dolphin.ui.cards.ItemRow import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.components.EditTextBox import com.github.damontecres.dolphin.ui.components.EditTextBox
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
@ -65,6 +67,7 @@ class SearchViewModel
searchTerm = query, searchTerm = query,
recursive = true, recursive = true,
includeItemTypes = listOf(BaseItemKind.MOVIE), includeItemTypes = listOf(BaseItemKind.MOVIE),
fields = DefaultItemFields,
limit = 25, limit = 25,
) )
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope) val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
@ -79,6 +82,7 @@ class SearchViewModel
searchTerm = query, searchTerm = query,
recursive = true, recursive = true,
includeItemTypes = listOf(BaseItemKind.SERIES), includeItemTypes = listOf(BaseItemKind.SERIES),
fields = DefaultItemFields,
limit = 25, limit = 25,
) )
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope) val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
@ -93,6 +97,7 @@ class SearchViewModel
searchTerm = query, searchTerm = query,
recursive = true, recursive = true,
includeItemTypes = listOf(BaseItemKind.EPISODE), includeItemTypes = listOf(BaseItemKind.EPISODE),
fields = DefaultItemFields,
limit = 25, limit = 25,
) )
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope) val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
@ -134,7 +139,7 @@ fun SearchPage(
} }
LazyColumn( LazyColumn(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 44.dp),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier, modifier = modifier,
) { ) {
@ -158,7 +163,7 @@ fun SearchPage(
) )
} }
} }
itemsIndexed(listOf(movies, series, episodes)) { index, items -> itemsIndexed(listOf(movies, series)) { index, items ->
if (items.isNotEmpty()) { if (items.isNotEmpty()) {
ItemRow( ItemRow(
title = title =
@ -177,5 +182,27 @@ fun SearchPage(
) )
} }
} }
if (episodes.isNotEmpty()) {
item {
ItemRow(
title = "Episodes",
items = episodes,
onClickItem = {
viewModel.navigationManager.navigateTo(it.destination())
},
onLongClickItem = {},
modifier = Modifier,
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
EpisodeCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageHeight = 160.dp,
modifier = mod,
)
},
)
}
}
} }
} }