feat: add initial focus management and media metadata for TV home screen

Introduce a `FocusableItem` interface to unify media item properties (ID, type, text, and images) across `ContinueWatchingItem`, `NextUpItem`, and `PosterItem`.

Key changes:
- Implement automatic initial focus on the first available item in `TvHomeContent`.
- Add `onMediaFocused` callbacks to TV sections and cards to track active items.
- Standardize image URL generation and metadata access in home models.
- Clean up unused previews in `HomeScreen`.
- Add UI tests for TV home content focus behavior.
This commit is contained in:
2026-04-05 17:38:19 +02:00
parent 584e430431
commit 0011420bf2
9 changed files with 355 additions and 76 deletions

View File

@@ -0,0 +1,108 @@
package hu.bbara.purefin.tv.home.ui
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.PosterItem
import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import org.junit.Rule
import org.junit.Test
import java.util.UUID
class TvHomeContentTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun tvHomeContent_focusesFirstAvailableItemByDefault() {
composeRule.setContent {
AppTheme {
TvHomeContent(
libraries = sampleLibraries(),
libraryContent = sampleLibraryContent(),
continueWatching = sampleContinueWatching(),
nextUp = emptyList(),
onMediaFocused = {},
onMovieSelected = {},
onSeriesSelected = {},
onEpisodeSelected = { _, _, _ -> }
)
}
}
composeRule.waitForIdle()
composeRule.onNodeWithTag(TvHomeInitialFocusTag).assertIsFocused()
}
private fun sampleContinueWatching(): List<ContinueWatchingItem> {
return listOf(
ContinueWatchingItem(
type = BaseItemKind.MOVIE,
movie = Movie(
id = UUID.fromString("11111111-1111-1111-1111-111111111111"),
libraryId = UUID.fromString("22222222-2222-2222-2222-222222222222"),
title = "Blade Runner 2049",
progress = 42.0,
watched = false,
year = "2017",
rating = "16+",
runtime = "164m",
format = "4K",
synopsis = "Officer K uncovers a secret that sends him searching for Rick Deckard.",
imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
audioTrack = "ENG 5.1",
subtitles = "ENG",
cast = emptyList()
)
)
)
}
private fun sampleLibraries(): List<LibraryItem> {
return listOf(
LibraryItem(
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
name = "Movies",
type = CollectionType.MOVIES,
posterUrl = "",
isEmpty = false
)
)
}
private fun sampleLibraryContent(): Map<UUID, List<PosterItem>> {
val libraryId = UUID.fromString("33333333-3333-3333-3333-333333333333")
return mapOf(
libraryId to listOf(
PosterItem(
type = BaseItemKind.MOVIE,
movie = Movie(
id = UUID.fromString("44444444-4444-4444-4444-444444444444"),
libraryId = libraryId,
title = "Arrival",
progress = null,
watched = false,
year = "2016",
rating = "12+",
runtime = "116m",
format = "4K",
synopsis = "A linguist works to communicate with mysterious visitors.",
imageUrlPrefix = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa",
audioTrack = "ENG 5.1",
subtitles = "ENG",
cast = emptyList()
)
)
)
)
}
}

View File

@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
import hu.bbara.purefin.feature.shared.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.PosterItem
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
@@ -39,6 +40,8 @@ import org.jellyfin.sdk.model.api.BaseItemKind
fun PosterCard(
item: PosterItem,
modifier: Modifier = Modifier,
imageModifier: Modifier = Modifier,
onFocusedItem: (FocusableItem) -> Unit = {},
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
@@ -74,7 +77,7 @@ fun PosterCard(
PurefinAsyncImage(
model = item.imageUrl,
contentDescription = null,
modifier = Modifier
modifier = imageModifier
.aspectRatio(2f / 3f)
.clip(RoundedCornerShape(14.dp))
.border(
@@ -83,7 +86,12 @@ fun PosterCard(
shape = RoundedCornerShape(14.dp)
)
.background(scheme.surfaceVariant)
.onFocusChanged { isFocused = it.isFocused }
.onFocusChanged {
isFocused = it.isFocused
if (it.isFocused) {
onFocusedItem(item)
}
}
.clickable(onClick = { openItem(item) }),
contentScale = ContentScale.Crop
)

View File

@@ -35,13 +35,15 @@ fun TvAppScreen(
libraryViewModel: LibraryViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
var selectedTabIndex by remember { mutableIntStateOf(1) }
val serverUrl by viewModel.serverUrl.collectAsState()
val libraries by viewModel.libraries.collectAsState()
val continueWatching by viewModel.continueWatching.collectAsState()
val nextUp by viewModel.nextUp.collectAsState()
val latestLibraryContent by viewModel.latestLibraryContent.collectAsState()
val selectedLibraryItems by libraryViewModel.contents.collectAsState()
var selectedTabIndex by remember { mutableIntStateOf(1) }
val tabs = remember(libraries) {
buildList {
add(
@@ -125,6 +127,7 @@ fun TvAppScreen(
libraryContent = latestLibraryContent,
continueWatching = continueWatching,
nextUp = nextUp,
serverUrl = serverUrl,
onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected,
onEpisodeSelected = viewModel::onEpisodeSelected,

View File

@@ -2,8 +2,13 @@ package hu.bbara.purefin.tv.home
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem
@@ -17,19 +22,26 @@ fun TvHomeScreen(
libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
serverUrl: String,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
var focusableItem by remember { mutableStateOf<FocusableItem?>(null) }
TvHomeContent(
libraries = libraries,
libraryContent = libraryContent,
continueWatching = continueWatching,
nextUp = nextUp,
onMediaFocused = { item: FocusableItem ->
focusableItem = item
},
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected,
modifier = modifier
)
}

View File

@@ -6,46 +6,78 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
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.runtime.withFrameNanos
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem
import org.jellyfin.sdk.model.UUID
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
@Composable
fun TvHomeContent(
libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
onMediaFocused: (FocusableItem) -> Unit,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
val visibleLibraries = remember(libraries, libraryContent) {
libraries.filter { libraryContent[it.id]?.isEmpty() != true }
}
val ( nextUpRef, continueWatchingRef ) = remember { FocusRequester.createRefs() }
val libraryRefs = remember(libraryContent) {
libraryContent.keys.associateWith { FocusRequester() }
val libraryRefs = remember(visibleLibraries) {
visibleLibraries.associate { it.id to FocusRequester() }
}
val initialFocusRequester = remember { FocusRequester() }
val firstVisibleLibraryId = visibleLibraries.firstOrNull()?.id
val firstAvailableItemKey = remember(
continueWatching,
nextUp,
firstVisibleLibraryId,
libraryContent
) {
when {
continueWatching.isNotEmpty() -> continueWatching.first().id
nextUp.isNotEmpty() -> nextUp.first().id
firstVisibleLibraryId != null -> libraryContent[firstVisibleLibraryId]?.firstOrNull()?.id
else -> null
}
}
var initialFocusApplied by remember { mutableStateOf(false) }
LaunchedEffect(firstAvailableItemKey, initialFocusApplied) {
if (!initialFocusApplied && firstAvailableItemKey != null) {
withFrameNanos { }
initialFocusRequester.requestFocus()
initialFocusApplied = true
}
}
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.background(Color.Transparent)
) {
item {
Spacer(modifier = Modifier.height(8.dp))
@@ -53,8 +85,11 @@ fun TvHomeContent(
item {
TvContinueWatchingSection(
items = continueWatching,
onFocusedItem = onMediaFocused,
onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected,
firstItemFocusRequester = initialFocusRequester.takeIf { continueWatching.isNotEmpty() },
firstItemTestTag = TvHomeInitialFocusTag.takeIf { continueWatching.isNotEmpty() },
modifier = Modifier.focusRequester(continueWatchingRef)
.focusProperties {
down = nextUpRef
@@ -67,7 +102,12 @@ fun TvHomeContent(
item {
TvNextUpSection(
items = nextUp,
onFocusedItem = onMediaFocused,
onEpisodeSelected = onEpisodeSelected,
firstItemFocusRequester = initialFocusRequester
.takeIf { continueWatching.isEmpty() && nextUp.isNotEmpty() },
firstItemTestTag = TvHomeInitialFocusTag
.takeIf { continueWatching.isEmpty() && nextUp.isNotEmpty() },
modifier = Modifier.focusRequester(nextUpRef)
.focusProperties {
up = continueWatchingRef
@@ -87,10 +127,21 @@ fun TvHomeContent(
title = item.name,
items = libraryContent[item.id] ?: emptyList(),
action = "See All",
onFocusedItem = onMediaFocused,
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected,
modifier = if (ref != null) Modifier.focusRequester(ref) else Modifier
firstItemFocusRequester = initialFocusRequester.takeIf {
continueWatching.isEmpty() &&
nextUp.isEmpty() &&
item.id == firstVisibleLibraryId
},
firstItemTestTag = TvHomeInitialFocusTag.takeIf {
continueWatching.isEmpty() &&
nextUp.isEmpty() &&
item.id == firstVisibleLibraryId
},
modifier = (if (ref != null) Modifier.focusRequester(ref) else Modifier)
.focusProperties {
up = nextUpRef
libraryRefs.values.dropWhile { it != ref }.drop(1).firstOrNull()
@@ -100,4 +151,4 @@ fun TvHomeContent(
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}

View File

@@ -28,10 +28,13 @@ 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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -41,6 +44,7 @@ import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem
import org.jellyfin.sdk.model.UUID
@@ -51,8 +55,11 @@ import kotlin.math.nextUp
@Composable
fun TvContinueWatchingSection(
items: List<ContinueWatchingItem>,
onFocusedItem: (FocusableItem) -> Unit = {},
onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
firstItemFocusRequester: FocusRequester? = null,
firstItemTestTag: String? = null,
modifier: Modifier = Modifier
) {
if (items.isEmpty()) return
@@ -66,9 +73,25 @@ fun TvContinueWatchingSection(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
itemsIndexed(items = items) { index, item ->
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
TvContinueWatchingCard(
item = item,
imageModifier = Modifier
.then(
if (index == 0 && firstItemFocusRequester != null) {
Modifier.focusRequester(firstItemFocusRequester)
} else {
Modifier
}
)
.then(
if (index == 0 && firstItemTestTag != null) {
Modifier.testTag(firstItemTestTag)
} else {
Modifier
}
),
onFocusedItem = onFocusedItem,
onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected
)
@@ -80,6 +103,8 @@ fun TvContinueWatchingSection(
fun TvContinueWatchingCard(
item: ContinueWatchingItem,
modifier: Modifier = Modifier,
imageModifier: Modifier = Modifier,
onFocusedItem: (FocusableItem) -> Unit = {},
onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) {
@@ -139,9 +164,14 @@ fun TvContinueWatchingCard(
PurefinAsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier
modifier = imageModifier
.fillMaxSize()
.onFocusChanged { isFocused = it.isFocused }
.onFocusChanged {
isFocused = it.isFocused
if (it.isFocused) {
onFocusedItem(item)
}
}
.clickable {
openItem(item)
},
@@ -178,7 +208,10 @@ fun TvContinueWatchingCard(
@Composable
fun TvNextUpSection(
items: List<NextUpItem>,
onFocusedItem: (FocusableItem) -> Unit = {},
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
firstItemFocusRequester: FocusRequester? = null,
firstItemTestTag: String? = null,
modifier: Modifier = Modifier
) {
if (items.isEmpty()) return
@@ -192,14 +225,25 @@ fun TvNextUpSection(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
itemsIndexed(
items = items,
key = { _, item -> item.id }
) { index, item ->
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
TvNextUpCard(
item = item,
isFirstItem = index == 0,
isLastItem = index == items.lastIndex,
imageModifier = Modifier
.then(
if (index == 0 && firstItemFocusRequester != null) {
Modifier.focusRequester(firstItemFocusRequester)
} else {
Modifier
}
)
.then(
if (index == 0 && firstItemTestTag != null) {
Modifier.testTag(firstItemTestTag)
} else {
Modifier
}
),
onFocusedItem = onFocusedItem,
onEpisodeSelected = onEpisodeSelected
)
}
@@ -210,8 +254,8 @@ fun TvNextUpSection(
fun TvNextUpCard(
item: NextUpItem,
modifier: Modifier = Modifier,
isFirstItem: Boolean = false,
isLastItem: Boolean = false,
imageModifier: Modifier = Modifier,
onFocusedItem: (FocusableItem) -> Unit = {},
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) {
val scheme = MaterialTheme.colorScheme
@@ -219,7 +263,7 @@ fun TvNextUpCard(
var isFocused by remember { mutableStateOf(false) }
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
val imageUrl = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY)
val imageUrl = item.imageUrl
val cardWidth = 280.dp
@@ -253,9 +297,14 @@ fun TvNextUpCard(
PurefinAsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier
modifier = imageModifier
.fillMaxSize()
.onFocusChanged { isFocused = it.isFocused }
.onFocusChanged {
isFocused = it.isFocused
if (it.isFocused) {
onFocusedItem(item)
}
}
.clickable {
openItem(item)
},
@@ -287,6 +336,9 @@ fun TvLibraryPosterSection(
title: String,
items: List<PosterItem>,
action: String?,
onFocusedItem: (FocusableItem) -> Unit = {},
firstItemFocusRequester: FocusRequester? = null,
firstItemTestTag: String? = null,
modifier: Modifier = Modifier,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
@@ -302,12 +354,25 @@ fun TvLibraryPosterSection(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
itemsIndexed(
items = items,
key = { _, item -> item.id }
) { index, item ->
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
PosterCard(
item = item,
imageModifier = Modifier
.then(
if (index == 0 && firstItemFocusRequester != null) {
Modifier.focusRequester(firstItemFocusRequester)
} else {
Modifier
}
)
.then(
if (index == 0 && firstItemTestTag != null) {
Modifier.testTag(firstItemTestTag)
} else {
Modifier
}
),
onFocusedItem = onFocusedItem,
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected

View File

@@ -13,20 +13,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import hu.bbara.purefin.app.home.ui.HomeContent
import hu.bbara.purefin.app.home.ui.HomeTopBar
import hu.bbara.purefin.app.home.ui.homePreviewContinueWatching
import hu.bbara.purefin.app.home.ui.homePreviewLibraries
import hu.bbara.purefin.app.home.ui.homePreviewLibraryContent
import hu.bbara.purefin.app.home.ui.homePreviewNextUp
import hu.bbara.purefin.app.home.ui.search.HomeSearchOverlay
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem
import hu.bbara.purefin.feature.shared.home.SuggestedItem
import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.UUID
@OptIn(ExperimentalMaterial3Api::class)
@@ -114,29 +108,4 @@ fun HomeScreen(
)
}
}
}
@Preview(name = "Home Screen", showBackground = true, widthDp = 412, heightDp = 915)
@Composable
private fun HomeScreenPreview() {
AppTheme(darkTheme = true) {
HomeScreen(
libraries = homePreviewLibraries(),
libraryContent = homePreviewLibraryContent(),
suggestions = emptyList(),
continueWatching = homePreviewContinueWatching(),
nextUp = homePreviewNextUp(),
isRefreshing = false,
onRefresh = {},
onMovieSelected = {},
onSeriesSelected = {},
onEpisodeSelected = { _, _, _ -> },
onLibrarySelected = {},
onProfileClick = {},
onSettingsClick = {},
onLogoutClick = {},
selectedTab = 0,
onTabSelected = {}
)
}
}
}

View File

@@ -39,6 +39,13 @@ class AppViewModel @Inject constructor(
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
val serverUrl: StateFlow<String> = userSessionRepository.serverUrl
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
""
)
val libraries = appContentRepository.libraries.map { libraries ->
libraries.map {
LibraryItem(

View File

@@ -88,26 +88,51 @@ data class SuggestedMovie (
)
) : SuggestedItem
sealed interface FocusableItem {
val imageUrl: String
val primaryText: String
val secondaryText: String
val description: String
val id: UUID
val type: BaseItemKind
}
data class ContinueWatchingItem(
val type: BaseItemKind,
override val type: BaseItemKind,
val movie: Movie? = null,
val episode: Episode? = null
) {
val id: UUID = when (type) {
val episode: Episode? = null,
) : FocusableItem {
override val id: UUID = when (type) {
BaseItemKind.MOVIE -> movie!!.id
BaseItemKind.EPISODE -> episode!!.id
else -> throw UnsupportedOperationException("Unsupported item type: $type")
}
val primaryText: String = when (type) {
override val primaryText: String = when (type) {
BaseItemKind.MOVIE -> movie!!.title
BaseItemKind.EPISODE -> episode!!.title
else -> throw UnsupportedOperationException("Unsupported item type: $type")
}
val secondaryText: String = when (type) {
override val secondaryText: String = when (type) {
BaseItemKind.MOVIE -> movie!!.year
BaseItemKind.EPISODE -> episode!!.releaseDate
else -> throw UnsupportedOperationException("Unsupported item type: $type")
}
override val imageUrl: String = when (type) {
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = movie!!.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = episode!!.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
else -> throw IllegalArgumentException("Invalid type: $type")
}
override val description: String = when (type) {
BaseItemKind.MOVIE -> movie!!.synopsis
BaseItemKind.EPISODE -> episode!!.synopsis
else -> throw UnsupportedOperationException("Unsupported item type: $type")
}
val progress: Double = when (type) {
BaseItemKind.MOVIE -> movie!!.progress ?: 0.0
BaseItemKind.EPISODE -> episode!!.progress ?: 0.0
@@ -117,10 +142,19 @@ data class ContinueWatchingItem(
data class NextUpItem(
val episode: Episode
) {
val id: UUID = episode.id
val primaryText: String = episode.title
val secondaryText: String = episode.releaseDate
) : FocusableItem {
override val id: UUID = episode.id
override val type: BaseItemKind
get() = BaseItemKind.EPISODE
override val imageUrl: String
get() = JellyfinImageHelper.finishImageUrl(
prefixImageUrl = episode.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
override val primaryText: String = episode.title
override val secondaryText: String = episode.releaseDate
override val description: String
get() = episode.synopsis
}
data class LibraryItem(
@@ -132,12 +166,12 @@ data class LibraryItem(
)
data class PosterItem(
val type: BaseItemKind,
override val type: BaseItemKind,
val movie: Movie? = null,
val series: Series? = null,
val episode: Episode? = null
) {
val id: UUID = when (type) {
) : FocusableItem {
override val id: UUID = when (type) {
BaseItemKind.MOVIE -> movie!!.id
BaseItemKind.EPISODE -> episode!!.id
BaseItemKind.SERIES -> series!!.id
@@ -149,7 +183,7 @@ data class PosterItem(
BaseItemKind.SERIES -> series!!.name
else -> throw IllegalArgumentException("Invalid type: $type")
}
val imageUrl: String = when (type) {
override val imageUrl: String = when (type) {
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = movie!!.imageUrlPrefix,
imageType = ImageType.PRIMARY
@@ -164,6 +198,28 @@ data class PosterItem(
)
else -> throw IllegalArgumentException("Invalid type: $type")
}
override val primaryText: String
get() = when (type) {
BaseItemKind.MOVIE -> movie!!.title
BaseItemKind.EPISODE -> episode!!.title
BaseItemKind.SERIES -> series!!.name
else -> throw IllegalArgumentException("Invalid type: $type")
}
override val secondaryText: String
get() = when (type) {
BaseItemKind.MOVIE -> movie!!.year
BaseItemKind.EPISODE -> episode!!.releaseDate
BaseItemKind.SERIES -> series!!.year
else -> throw IllegalArgumentException("Invalid type: $type")
}
override val description: String
get() = when (type) {
BaseItemKind.MOVIE -> movie!!.synopsis
BaseItemKind.EPISODE -> episode!!.synopsis
BaseItemKind.SERIES -> series!!.synopsis
else -> throw IllegalArgumentException("Invalid type: $type")
}
fun watched() = when (type) {
BaseItemKind.MOVIE -> movie!!.watched
BaseItemKind.EPISODE -> episode!!.watched