diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt index 694b84ce..eee72392 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt @@ -29,12 +29,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import hu.bbara.purefin.ui.common.badge.WatchStateBadge +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.model.EpisodeUiModel import hu.bbara.purefin.ui.model.MediaUiModel import hu.bbara.purefin.ui.model.MovieUiModel import hu.bbara.purefin.ui.model.SeriesUiModel -import hu.bbara.purefin.ui.common.badge.WatchStateBadge -import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import java.util.UUID @Composable @@ -43,7 +43,6 @@ fun PosterCard( modifier: Modifier = Modifier, imageModifier: Modifier = Modifier, posterWidth: Dp = 144.dp, - contentScale: Float = 1f, showSecondaryText: Boolean = false, indicatorSize: Int = 28, indicatorPadding: Dp = 8.dp, @@ -65,7 +64,6 @@ fun PosterCard( modifier = modifier, imageModifier = imageModifier, posterWidth = posterWidth, - contentScale = contentScale, showSecondaryText = showSecondaryText, indicatorSize = indicatorSize, indicatorPadding = indicatorPadding, @@ -82,11 +80,10 @@ fun PosterCardContent( modifier: Modifier = Modifier, imageModifier: Modifier = Modifier, posterWidth: Dp = 144.dp, - contentScale: Float = 1f, showSecondaryText: Boolean = false, indicatorSize: Int = 28, indicatorPadding: Dp = 8.dp, - onFocused: () -> Unit = {}, + onFocused: () -> Unit, focusedScale: Float = 1f, focusedBorderWidth: Dp = 1.dp, focusedTransformOrigin: TransformOrigin = TransformOrigin(0.5f, 0f) diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt index e988946d..70c1e82e 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt @@ -20,11 +20,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp -import hu.bbara.purefin.ui.model.MediaUiModel import hu.bbara.purefin.ui.model.LibraryUiModel +import hu.bbara.purefin.ui.model.MediaUiModel import java.util.UUID internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item" @@ -46,9 +46,6 @@ fun TvHomeContent( val hasContinueWatching = continueWatching.isNotEmpty() val hasNextUp = nextUp.isNotEmpty() val hasLibraryItems = libraries.any { libraryContent[it.id].orEmpty().isNotEmpty() } - val firstLibraryWithItemsId = libraries.firstOrNull { - libraryContent[it.id].orEmpty().isNotEmpty() - }?.id val hasVisibleContent = hasContinueWatching || hasNextUp val hasInitialFocusableItem = hasContinueWatching || hasNextUp || hasLibraryItems val initialFocusRequester = remember { FocusRequester() } @@ -68,7 +65,7 @@ fun TvHomeContent( modifier = modifier .fillMaxSize() .background(scheme.background) - .testTag(TvHomeContentViewportTag), + .focusRequester(initialFocusRequester), contentPadding = contentPadding ) { item(key = "tv-home-top-spacer") { @@ -80,9 +77,6 @@ fun TvHomeContent( items = continueWatching, onFocusedItem = onMediaFocused, onMediaSelected = onMediaSelected, - firstItemFocusRequester = initialFocusRequester, - firstItemTestTag = TvHomeInitialFocusTag, - rowTestTag = TvHomeContinueWatchingRowTag ) } } @@ -93,9 +87,6 @@ fun TvHomeContent( items = nextUp, onFocusedItem = onMediaFocused, onMediaSelected = onMediaSelected, - firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching }, - firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching }, - rowTestTag = TvHomeNextUpRowTag ) } } @@ -108,10 +99,6 @@ fun TvHomeContent( title = library.name, items = libraryContent[library.id].orEmpty(), onFocusedItem = onMediaFocused, - firstItemFocusRequester = initialFocusRequester.takeIf { - !hasContinueWatching && !hasNextUp && library.id == firstLibraryWithItemsId - }, - firstItemTestTag = tvHomeLibraryFirstItemTag(library.id), onMediaSelected = onMediaSelected ) } diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeSections.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeSections.kt index e782a66c..2b33cb72 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeSections.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeSections.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.gestures.LocalBringIntoViewSpec import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -17,9 +18,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -28,26 +29,27 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable 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.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush 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 import androidx.compose.ui.unit.sp -import hu.bbara.purefin.ui.model.MediaUiModel import hu.bbara.purefin.ui.common.bar.MediaProgressBar import hu.bbara.purefin.ui.common.card.PosterCardContent import hu.bbara.purefin.ui.common.image.PurefinAsyncImage +import hu.bbara.purefin.ui.model.MediaUiModel import java.util.UUID import kotlin.math.roundToInt @@ -69,7 +71,6 @@ private val TvHomeLandscapeSupportingFontSize = (11f * TvHomeMediaCardScale).sp private val TvHomePosterIndicatorSize = (24f * TvHomeMediaCardScale).roundToInt() private val TvHomePosterIndicatorPadding = (6f * TvHomeMediaCardScale).dp internal const val TvHomeSectionRowTagPrefix = "tv-home-section-row-" -internal const val TvHomeContinueWatchingRowTag = "${TvHomeSectionRowTagPrefix}continue-watching" internal const val TvHomeNextUpRowTag = "${TvHomeSectionRowTagPrefix}next-up" internal fun tvHomeLibraryRowTag(libraryId: UUID): String = @@ -81,57 +82,35 @@ internal fun tvHomeLibraryFirstItemTag(libraryId: UUID): String = @Composable fun TvContinueWatchingSection( items: List, - onFocusedItem: (MediaUiModel) -> Unit = {}, + onFocusedItem: (MediaUiModel) -> Unit, onMediaSelected: (MediaUiModel) -> Unit, - firstItemFocusRequester: FocusRequester? = null, - firstItemTestTag: String? = null, - rowTestTag: String? = null, modifier: Modifier = Modifier ) { if (items.isEmpty()) return - TvSectionHeader( - title = "Continue Watching", - ) TvHomeSectionRow( + items = items, + onFocused = onFocusedItem, modifier = modifier, - rowTestTag = rowTestTag - ) { - itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> - TvHomeLandscapeCard( - title = item.primaryText, - supporting = item.secondaryText, - imageUrl = item.primaryImageUrl, - progress = item.progress ?: 0f, - 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(item) }, - onClick = { onMediaSelected(item) } - ) - } + ) { item, onFocused, focusModifier -> + TvHomeLandscapeCard( + title = item.primaryText, + supporting = item.secondaryText, + imageUrl = item.primaryImageUrl, + progress = item.progress ?: 0f, + imageModifier = focusModifier, + onFocusedItem = { + onFocused(item) + }, + onClick = { onMediaSelected(item) } + ) } } @Composable fun TvNextUpSection( items: List, - onFocusedItem: (MediaUiModel) -> Unit = {}, + onFocusedItem: (MediaUiModel) -> Unit, onMediaSelected: (MediaUiModel) -> Unit, - firstItemFocusRequester: FocusRequester? = null, - firstItemTestTag: String? = null, - rowTestTag: String? = null, modifier: Modifier = Modifier ) { if (items.isEmpty()) return @@ -139,33 +118,18 @@ fun TvNextUpSection( title = "Next Up", ) TvHomeSectionRow( + items = items, + onFocused = onFocusedItem, modifier = modifier, - rowTestTag = rowTestTag - ) { - itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> - TvHomeLandscapeCard( - title = item.primaryText, - supporting = item.secondaryText, - imageUrl = item.primaryImageUrl, - 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(item) }, - onClick = { onMediaSelected(item) } - ) - } + ) { item, onFocused, focusModifier -> + TvHomeLandscapeCard( + title = item.primaryText, + supporting = item.secondaryText, + imageUrl = item.primaryImageUrl, + imageModifier = focusModifier, + onFocusedItem = { onFocused(item) }, + onClick = { onMediaSelected(item) } + ) } } @@ -173,74 +137,75 @@ fun TvNextUpSection( fun TvLibraryPosterSection( title: String, items: List, - onFocusedItem: (MediaUiModel) -> Unit = {}, - firstItemFocusRequester: FocusRequester? = null, - firstItemTestTag: String? = null, - rowTestTag: String? = null, + onFocusedItem: (MediaUiModel) -> Unit, modifier: Modifier = Modifier, onMediaSelected: (MediaUiModel) -> Unit, ) { + if (items.isEmpty()) return + TvSectionHeader( title = title, ) TvHomeSectionRow( + items = items, + onFocused = onFocusedItem, modifier = modifier, - rowTestTag = rowTestTag - ) { - itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> - PosterCardContent( - model = item, - onClick = { onMediaSelected(item) }, - posterWidth = TvHomePosterCardWidth, - contentScale = TvHomeMediaCardScale, - showSecondaryText = true, - indicatorSize = TvHomePosterIndicatorSize, - indicatorPadding = TvHomePosterIndicatorPadding, - imageModifier = Modifier - .then( - if (index == 0 && firstItemFocusRequester != null) { - Modifier.focusRequester(firstItemFocusRequester) - } else { - Modifier - } - ) - .then( - if (index == 0 && firstItemTestTag != null) { - Modifier.testTag(firstItemTestTag) - } else { - Modifier - } - ), - onFocused = { onFocusedItem(item) }, - focusedScale = 1.07f, - focusedBorderWidth = 2.dp - ) - } + ) { item, onFocused, focusModifier -> + PosterCardContent( + model = item, + onClick = { onMediaSelected(item) }, + onFocused = { onFocused(item) }, + posterWidth = TvHomePosterCardWidth, + showSecondaryText = true, + indicatorSize = TvHomePosterIndicatorSize, + indicatorPadding = TvHomePosterIndicatorPadding, + imageModifier = focusModifier, + focusedScale = 1.07f, + focusedBorderWidth = 2.dp + ) } } @OptIn(ExperimentalFoundationApi::class) @Composable private fun TvHomeSectionRow( + items: List, + onFocused: (MediaUiModel) -> Unit, modifier: Modifier = Modifier, - rowTestTag: String? = null, - content: LazyListScope.() -> Unit, + childComponent: @Composable ( + item: MediaUiModel, + onFocused: (MediaUiModel) -> Unit, + focusModifier: Modifier, + ) -> Unit, ) { + val listState = rememberLazyListState() + + var focusedItemId by rememberSaveable { mutableStateOf(items.first().id) } + val rowScopedFocusRequester = remember { FocusRequester() } + CompositionLocalProvider(LocalBringIntoViewSpec provides TvHomeRowBringIntoViewSpec) { LazyRow( + state = listState, modifier = modifier .fillMaxWidth() - .then( - if (rowTestTag != null) { - Modifier.testTag(rowTestTag) - } else { - Modifier - } - ), + .focusRestorer(rowScopedFocusRequester) + .focusGroup(), contentPadding = PaddingValues(horizontal = TvHomeSectionsHorizontalPadding), - horizontalArrangement = Arrangement.spacedBy(TvHomeSectionsRowSpacing), - content = content - ) + horizontalArrangement = Arrangement.spacedBy(TvHomeSectionsRowSpacing) + ) { + itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> + val focusModifier = + if (item.id == focusedItemId) Modifier.focusRequester(rowScopedFocusRequester) else Modifier + childComponent( + item, + { + focusedItemId = item.id + onFocused(item) + }, + focusModifier + ) + } + } } } @@ -279,7 +244,10 @@ private fun TvHomeLandscapeCard( ) { val scheme = MaterialTheme.colorScheme var isFocused by remember { mutableStateOf(false) } - val scale by animateFloatAsState(targetValue = if (isFocused) 1.055f else 1f, label = "tv-home-landscape-scale") + val scale by animateFloatAsState( + targetValue = if (isFocused) 1.055f else 1f, + label = "tv-home-landscape-scale" + ) val cardWidth = TvHomeLandscapeCardWidth Column(