mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
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:
@@ -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()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
|
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
|
||||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
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 hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
@@ -39,6 +40,8 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
|||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
item: PosterItem,
|
item: PosterItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
imageModifier: Modifier = Modifier,
|
||||||
|
onFocusedItem: (FocusableItem) -> Unit = {},
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
@@ -74,7 +77,7 @@ fun PosterCard(
|
|||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = item.imageUrl,
|
model = item.imageUrl,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = imageModifier
|
||||||
.aspectRatio(2f / 3f)
|
.aspectRatio(2f / 3f)
|
||||||
.clip(RoundedCornerShape(14.dp))
|
.clip(RoundedCornerShape(14.dp))
|
||||||
.border(
|
.border(
|
||||||
@@ -83,7 +86,12 @@ fun PosterCard(
|
|||||||
shape = RoundedCornerShape(14.dp)
|
shape = RoundedCornerShape(14.dp)
|
||||||
)
|
)
|
||||||
.background(scheme.surfaceVariant)
|
.background(scheme.surfaceVariant)
|
||||||
.onFocusChanged { isFocused = it.isFocused }
|
.onFocusChanged {
|
||||||
|
isFocused = it.isFocused
|
||||||
|
if (it.isFocused) {
|
||||||
|
onFocusedItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
.clickable(onClick = { openItem(item) }),
|
.clickable(onClick = { openItem(item) }),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,13 +35,15 @@ fun TvAppScreen(
|
|||||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var selectedTabIndex by remember { mutableIntStateOf(1) }
|
val serverUrl by viewModel.serverUrl.collectAsState()
|
||||||
val libraries by viewModel.libraries.collectAsState()
|
val libraries by viewModel.libraries.collectAsState()
|
||||||
val continueWatching by viewModel.continueWatching.collectAsState()
|
val continueWatching by viewModel.continueWatching.collectAsState()
|
||||||
val nextUp by viewModel.nextUp.collectAsState()
|
val nextUp by viewModel.nextUp.collectAsState()
|
||||||
val latestLibraryContent by viewModel.latestLibraryContent.collectAsState()
|
val latestLibraryContent by viewModel.latestLibraryContent.collectAsState()
|
||||||
val selectedLibraryItems by libraryViewModel.contents.collectAsState()
|
val selectedLibraryItems by libraryViewModel.contents.collectAsState()
|
||||||
|
|
||||||
|
var selectedTabIndex by remember { mutableIntStateOf(1) }
|
||||||
|
|
||||||
val tabs = remember(libraries) {
|
val tabs = remember(libraries) {
|
||||||
buildList {
|
buildList {
|
||||||
add(
|
add(
|
||||||
@@ -125,6 +127,7 @@ fun TvAppScreen(
|
|||||||
libraryContent = latestLibraryContent,
|
libraryContent = latestLibraryContent,
|
||||||
continueWatching = continueWatching,
|
continueWatching = continueWatching,
|
||||||
nextUp = nextUp,
|
nextUp = nextUp,
|
||||||
|
serverUrl = serverUrl,
|
||||||
onMovieSelected = viewModel::onMovieSelected,
|
onMovieSelected = viewModel::onMovieSelected,
|
||||||
onSeriesSelected = viewModel::onSeriesSelected,
|
onSeriesSelected = viewModel::onSeriesSelected,
|
||||||
onEpisodeSelected = viewModel::onEpisodeSelected,
|
onEpisodeSelected = viewModel::onEpisodeSelected,
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ package hu.bbara.purefin.tv.home
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.runtime.Composable
|
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 androidx.compose.ui.Modifier
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
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.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
@@ -17,19 +22,26 @@ fun TvHomeScreen(
|
|||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<PosterItem>>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<ContinueWatchingItem>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<NextUpItem>,
|
||||||
|
serverUrl: String,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
var focusableItem by remember { mutableStateOf<FocusableItem?>(null) }
|
||||||
|
|
||||||
TvHomeContent(
|
TvHomeContent(
|
||||||
libraries = libraries,
|
libraries = libraries,
|
||||||
libraryContent = libraryContent,
|
libraryContent = libraryContent,
|
||||||
continueWatching = continueWatching,
|
continueWatching = continueWatching,
|
||||||
nextUp = nextUp,
|
nextUp = nextUp,
|
||||||
|
onMediaFocused = { item: FocusableItem ->
|
||||||
|
focusableItem = item
|
||||||
|
},
|
||||||
onMovieSelected = onMovieSelected,
|
onMovieSelected = onMovieSelected,
|
||||||
onSeriesSelected = onSeriesSelected,
|
onSeriesSelected = onSeriesSelected,
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
onEpisodeSelected = onEpisodeSelected,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,46 +6,78 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
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.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.withFrameNanos
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
|
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
|
||||||
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
|
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
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.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvHomeContent(
|
fun TvHomeContent(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryItem>,
|
||||||
libraryContent: Map<UUID, List<PosterItem>>,
|
libraryContent: Map<UUID, List<PosterItem>>,
|
||||||
continueWatching: List<ContinueWatchingItem>,
|
continueWatching: List<ContinueWatchingItem>,
|
||||||
nextUp: List<NextUpItem>,
|
nextUp: List<NextUpItem>,
|
||||||
|
onMediaFocused: (FocusableItem) -> Unit,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val visibleLibraries = remember(libraries, libraryContent) {
|
val visibleLibraries = remember(libraries, libraryContent) {
|
||||||
libraries.filter { libraryContent[it.id]?.isEmpty() != true }
|
libraries.filter { libraryContent[it.id]?.isEmpty() != true }
|
||||||
}
|
}
|
||||||
|
|
||||||
val ( nextUpRef, continueWatchingRef ) = remember { FocusRequester.createRefs() }
|
val ( nextUpRef, continueWatchingRef ) = remember { FocusRequester.createRefs() }
|
||||||
val libraryRefs = remember(libraryContent) {
|
val libraryRefs = remember(visibleLibraries) {
|
||||||
libraryContent.keys.associateWith { FocusRequester() }
|
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(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(Color.Transparent)
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -53,8 +85,11 @@ fun TvHomeContent(
|
|||||||
item {
|
item {
|
||||||
TvContinueWatchingSection(
|
TvContinueWatchingSection(
|
||||||
items = continueWatching,
|
items = continueWatching,
|
||||||
|
onFocusedItem = onMediaFocused,
|
||||||
onMovieSelected = onMovieSelected,
|
onMovieSelected = onMovieSelected,
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
onEpisodeSelected = onEpisodeSelected,
|
||||||
|
firstItemFocusRequester = initialFocusRequester.takeIf { continueWatching.isNotEmpty() },
|
||||||
|
firstItemTestTag = TvHomeInitialFocusTag.takeIf { continueWatching.isNotEmpty() },
|
||||||
modifier = Modifier.focusRequester(continueWatchingRef)
|
modifier = Modifier.focusRequester(continueWatchingRef)
|
||||||
.focusProperties {
|
.focusProperties {
|
||||||
down = nextUpRef
|
down = nextUpRef
|
||||||
@@ -67,7 +102,12 @@ fun TvHomeContent(
|
|||||||
item {
|
item {
|
||||||
TvNextUpSection(
|
TvNextUpSection(
|
||||||
items = nextUp,
|
items = nextUp,
|
||||||
|
onFocusedItem = onMediaFocused,
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
onEpisodeSelected = onEpisodeSelected,
|
||||||
|
firstItemFocusRequester = initialFocusRequester
|
||||||
|
.takeIf { continueWatching.isEmpty() && nextUp.isNotEmpty() },
|
||||||
|
firstItemTestTag = TvHomeInitialFocusTag
|
||||||
|
.takeIf { continueWatching.isEmpty() && nextUp.isNotEmpty() },
|
||||||
modifier = Modifier.focusRequester(nextUpRef)
|
modifier = Modifier.focusRequester(nextUpRef)
|
||||||
.focusProperties {
|
.focusProperties {
|
||||||
up = continueWatchingRef
|
up = continueWatchingRef
|
||||||
@@ -87,10 +127,21 @@ fun TvHomeContent(
|
|||||||
title = item.name,
|
title = item.name,
|
||||||
items = libraryContent[item.id] ?: emptyList(),
|
items = libraryContent[item.id] ?: emptyList(),
|
||||||
action = "See All",
|
action = "See All",
|
||||||
|
onFocusedItem = onMediaFocused,
|
||||||
onMovieSelected = onMovieSelected,
|
onMovieSelected = onMovieSelected,
|
||||||
onSeriesSelected = onSeriesSelected,
|
onSeriesSelected = onSeriesSelected,
|
||||||
onEpisodeSelected = onEpisodeSelected,
|
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 {
|
.focusProperties {
|
||||||
up = nextUpRef
|
up = nextUpRef
|
||||||
libraryRefs.values.dropWhile { it != ref }.drop(1).firstOrNull()
|
libraryRefs.values.dropWhile { it != ref }.drop(1).firstOrNull()
|
||||||
@@ -100,4 +151,4 @@ fun TvHomeContent(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,13 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.common.ui.components.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
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.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
@@ -51,8 +55,11 @@ import kotlin.math.nextUp
|
|||||||
@Composable
|
@Composable
|
||||||
fun TvContinueWatchingSection(
|
fun TvContinueWatchingSection(
|
||||||
items: List<ContinueWatchingItem>,
|
items: List<ContinueWatchingItem>,
|
||||||
|
onFocusedItem: (FocusableItem) -> Unit = {},
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
|
firstItemTestTag: String? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
@@ -66,9 +73,25 @@ fun TvContinueWatchingSection(
|
|||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(items = items) { index, item ->
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
TvContinueWatchingCard(
|
TvContinueWatchingCard(
|
||||||
item = item,
|
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,
|
onMovieSelected = onMovieSelected,
|
||||||
onEpisodeSelected = onEpisodeSelected
|
onEpisodeSelected = onEpisodeSelected
|
||||||
)
|
)
|
||||||
@@ -80,6 +103,8 @@ fun TvContinueWatchingSection(
|
|||||||
fun TvContinueWatchingCard(
|
fun TvContinueWatchingCard(
|
||||||
item: ContinueWatchingItem,
|
item: ContinueWatchingItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
imageModifier: Modifier = Modifier,
|
||||||
|
onFocusedItem: (FocusableItem) -> Unit = {},
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -139,9 +164,14 @@ fun TvContinueWatchingCard(
|
|||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = imageUrl,
|
model = imageUrl,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = imageModifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.onFocusChanged { isFocused = it.isFocused }
|
.onFocusChanged {
|
||||||
|
isFocused = it.isFocused
|
||||||
|
if (it.isFocused) {
|
||||||
|
onFocusedItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
.clickable {
|
.clickable {
|
||||||
openItem(item)
|
openItem(item)
|
||||||
},
|
},
|
||||||
@@ -178,7 +208,10 @@ fun TvContinueWatchingCard(
|
|||||||
@Composable
|
@Composable
|
||||||
fun TvNextUpSection(
|
fun TvNextUpSection(
|
||||||
items: List<NextUpItem>,
|
items: List<NextUpItem>,
|
||||||
|
onFocusedItem: (FocusableItem) -> Unit = {},
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
|
firstItemTestTag: String? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
@@ -192,14 +225,25 @@ fun TvNextUpSection(
|
|||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
items = items,
|
|
||||||
key = { _, item -> item.id }
|
|
||||||
) { index, item ->
|
|
||||||
TvNextUpCard(
|
TvNextUpCard(
|
||||||
item = item,
|
item = item,
|
||||||
isFirstItem = index == 0,
|
imageModifier = Modifier
|
||||||
isLastItem = index == items.lastIndex,
|
.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
|
onEpisodeSelected = onEpisodeSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -210,8 +254,8 @@ fun TvNextUpSection(
|
|||||||
fun TvNextUpCard(
|
fun TvNextUpCard(
|
||||||
item: NextUpItem,
|
item: NextUpItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isFirstItem: Boolean = false,
|
imageModifier: Modifier = Modifier,
|
||||||
isLastItem: Boolean = false,
|
onFocusedItem: (FocusableItem) -> Unit = {},
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -219,7 +263,7 @@ fun TvNextUpCard(
|
|||||||
var isFocused by remember { mutableStateOf(false) }
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
|
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
|
val cardWidth = 280.dp
|
||||||
|
|
||||||
@@ -253,9 +297,14 @@ fun TvNextUpCard(
|
|||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = imageUrl,
|
model = imageUrl,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = imageModifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.onFocusChanged { isFocused = it.isFocused }
|
.onFocusChanged {
|
||||||
|
isFocused = it.isFocused
|
||||||
|
if (it.isFocused) {
|
||||||
|
onFocusedItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
.clickable {
|
.clickable {
|
||||||
openItem(item)
|
openItem(item)
|
||||||
},
|
},
|
||||||
@@ -287,6 +336,9 @@ fun TvLibraryPosterSection(
|
|||||||
title: String,
|
title: String,
|
||||||
items: List<PosterItem>,
|
items: List<PosterItem>,
|
||||||
action: String?,
|
action: String?,
|
||||||
|
onFocusedItem: (FocusableItem) -> Unit = {},
|
||||||
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
|
firstItemTestTag: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onMovieSelected: (UUID) -> Unit,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
@@ -302,12 +354,25 @@ fun TvLibraryPosterSection(
|
|||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
items = items,
|
|
||||||
key = { _, item -> item.id }
|
|
||||||
) { index, item ->
|
|
||||||
PosterCard(
|
PosterCard(
|
||||||
item = item,
|
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,
|
onMovieSelected = onMovieSelected,
|
||||||
onSeriesSelected = onSeriesSelected,
|
onSeriesSelected = onSeriesSelected,
|
||||||
onEpisodeSelected = onEpisodeSelected
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
|||||||
@@ -13,20 +13,14 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
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.HomeContent
|
||||||
import hu.bbara.purefin.app.home.ui.HomeTopBar
|
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.app.home.ui.search.HomeSearchOverlay
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@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 = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,6 +39,13 @@ class AppViewModel @Inject constructor(
|
|||||||
private val _isRefreshing = MutableStateFlow(false)
|
private val _isRefreshing = MutableStateFlow(false)
|
||||||
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
|
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
|
||||||
|
|
||||||
|
val serverUrl: StateFlow<String> = userSessionRepository.serverUrl
|
||||||
|
.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
val libraries = appContentRepository.libraries.map { libraries ->
|
val libraries = appContentRepository.libraries.map { libraries ->
|
||||||
libraries.map {
|
libraries.map {
|
||||||
LibraryItem(
|
LibraryItem(
|
||||||
|
|||||||
@@ -88,26 +88,51 @@ data class SuggestedMovie (
|
|||||||
)
|
)
|
||||||
) : SuggestedItem
|
) : 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(
|
data class ContinueWatchingItem(
|
||||||
val type: BaseItemKind,
|
override val type: BaseItemKind,
|
||||||
val movie: Movie? = null,
|
val movie: Movie? = null,
|
||||||
val episode: Episode? = null
|
val episode: Episode? = null,
|
||||||
) {
|
) : FocusableItem {
|
||||||
val id: UUID = when (type) {
|
override val id: UUID = when (type) {
|
||||||
BaseItemKind.MOVIE -> movie!!.id
|
BaseItemKind.MOVIE -> movie!!.id
|
||||||
BaseItemKind.EPISODE -> episode!!.id
|
BaseItemKind.EPISODE -> episode!!.id
|
||||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||||
}
|
}
|
||||||
val primaryText: String = when (type) {
|
override val primaryText: String = when (type) {
|
||||||
BaseItemKind.MOVIE -> movie!!.title
|
BaseItemKind.MOVIE -> movie!!.title
|
||||||
BaseItemKind.EPISODE -> episode!!.title
|
BaseItemKind.EPISODE -> episode!!.title
|
||||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
||||||
}
|
}
|
||||||
val secondaryText: String = when (type) {
|
override val secondaryText: String = when (type) {
|
||||||
BaseItemKind.MOVIE -> movie!!.year
|
BaseItemKind.MOVIE -> movie!!.year
|
||||||
BaseItemKind.EPISODE -> episode!!.releaseDate
|
BaseItemKind.EPISODE -> episode!!.releaseDate
|
||||||
else -> throw UnsupportedOperationException("Unsupported item type: $type")
|
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) {
|
val progress: Double = when (type) {
|
||||||
BaseItemKind.MOVIE -> movie!!.progress ?: 0.0
|
BaseItemKind.MOVIE -> movie!!.progress ?: 0.0
|
||||||
BaseItemKind.EPISODE -> episode!!.progress ?: 0.0
|
BaseItemKind.EPISODE -> episode!!.progress ?: 0.0
|
||||||
@@ -117,10 +142,19 @@ data class ContinueWatchingItem(
|
|||||||
|
|
||||||
data class NextUpItem(
|
data class NextUpItem(
|
||||||
val episode: Episode
|
val episode: Episode
|
||||||
) {
|
) : FocusableItem {
|
||||||
val id: UUID = episode.id
|
override val id: UUID = episode.id
|
||||||
val primaryText: String = episode.title
|
override val type: BaseItemKind
|
||||||
val secondaryText: String = episode.releaseDate
|
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(
|
data class LibraryItem(
|
||||||
@@ -132,12 +166,12 @@ data class LibraryItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class PosterItem(
|
data class PosterItem(
|
||||||
val type: BaseItemKind,
|
override val type: BaseItemKind,
|
||||||
val movie: Movie? = null,
|
val movie: Movie? = null,
|
||||||
val series: Series? = null,
|
val series: Series? = null,
|
||||||
val episode: Episode? = null
|
val episode: Episode? = null
|
||||||
) {
|
) : FocusableItem {
|
||||||
val id: UUID = when (type) {
|
override val id: UUID = when (type) {
|
||||||
BaseItemKind.MOVIE -> movie!!.id
|
BaseItemKind.MOVIE -> movie!!.id
|
||||||
BaseItemKind.EPISODE -> episode!!.id
|
BaseItemKind.EPISODE -> episode!!.id
|
||||||
BaseItemKind.SERIES -> series!!.id
|
BaseItemKind.SERIES -> series!!.id
|
||||||
@@ -149,7 +183,7 @@ data class PosterItem(
|
|||||||
BaseItemKind.SERIES -> series!!.name
|
BaseItemKind.SERIES -> series!!.name
|
||||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||||
}
|
}
|
||||||
val imageUrl: String = when (type) {
|
override val imageUrl: String = when (type) {
|
||||||
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||||
prefixImageUrl = movie!!.imageUrlPrefix,
|
prefixImageUrl = movie!!.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
imageType = ImageType.PRIMARY
|
||||||
@@ -164,6 +198,28 @@ data class PosterItem(
|
|||||||
)
|
)
|
||||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
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) {
|
fun watched() = when (type) {
|
||||||
BaseItemKind.MOVIE -> movie!!.watched
|
BaseItemKind.MOVIE -> movie!!.watched
|
||||||
BaseItemKind.EPISODE -> episode!!.watched
|
BaseItemKind.EPISODE -> episode!!.watched
|
||||||
|
|||||||
Reference in New Issue
Block a user