mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +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.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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user