diff --git a/AdaptiveJetStream/gradle/libs.versions.toml b/AdaptiveJetStream/gradle/libs.versions.toml index 6f2886f..61e3b6d 100644 --- a/AdaptiveJetStream/gradle/libs.versions.toml +++ b/AdaptiveJetStream/gradle/libs.versions.toml @@ -1,31 +1,30 @@ [versions] activity-compose = "1.10.1" -android-gradle-plugin = "8.9.3" -android-test-plugin = "8.9.3" -androidx-baselineprofile = "1.3.4" -benchmark-macro-junit4 = "1.3.4" +android-gradle-plugin = "8.12.1" +android-test-plugin = "8.12.1" +androidx-baselineprofile = "1.4.0" +benchmark-macro-junit4 = "1.4.0" coil-compose = "2.7.0" -compose-bom = "2025.05.00" -tv-material = "1.0.0" -core-ktx = "1.16.0" +compose-bom = "2025.08.00" +core-ktx = "1.17.0" core-splashscreen = "1.0.1" hilt-navigation-compose = "1.2.0" -hilt-android = "2.56.1" -junit = "1.2.1" -kotlin-android = "2.1.10" -kotlinx-serialization = "1.8.1" -ksp = "2.1.10-1.0.30" -lifecycle-runtime-ktx = "2.9.0" +hilt-android = "2.57" +junit = "1.3.0" +kotlin-android = "2.2.10" +kotlinx-serialization = "1.9.0" +ksp = "2.2.10-2.0.2" +lifecycle-runtime-ktx = "2.9.2" material3-adaptive = "1.1.0" -material3-adaptive-navigation = "1.4.0-alpha14" -media3 = "1.6.1" -navigation-compose = "2.9.0" +material3-adaptive-navigation = "1.4.0-beta02" +media3 = "1.8.0" +navigation-compose = "2.9.3" profileinstaller = "1.4.1" uiautomator = "2.3.0" -rules = "1.6.1" -window = "1.4.0-rc02" -xr = "1.0.0-alpha04" -xr-material3 = "1.0.0-alpha08" +rules = "1.7.0" +window = "1.4.0" +xr = "1.0.0-alpha06" +xr-material3 = "1.0.0-alpha10" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } @@ -52,7 +51,6 @@ androidx-media3-ui = { module = "androidx.media3:media3-ui-compose", version.ref androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" } -androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "tv-material" } androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" } diff --git a/AdaptiveJetStream/gradle/wrapper/gradle-wrapper.properties b/AdaptiveJetStream/gradle/wrapper/gradle-wrapper.properties index 4eaec46..c6f0030 100644 --- a/AdaptiveJetStream/gradle/wrapper/gradle-wrapper.properties +++ b/AdaptiveJetStream/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/AdaptiveJetStream/jetstream/build.gradle.kts b/AdaptiveJetStream/jetstream/build.gradle.kts index 5fc98bd..1913e0a 100644 --- a/AdaptiveJetStream/jetstream/build.gradle.kts +++ b/AdaptiveJetStream/jetstream/build.gradle.kts @@ -31,12 +31,12 @@ kotlin { android { namespace = "com.google.jetstream" // Needed for latest androidx snapshot build - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "com.google.jetstream" minSdk = 28 - targetSdk = 35 + targetSdk = 36 versionCode = 1 versionName = "1.0" @@ -67,8 +67,9 @@ android { } } - kotlinOptions { - jvmTarget = "17" + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } @@ -83,9 +84,6 @@ dependencies { // extra material icons implementation(libs.androidx.material.icons.extended) - // Material components optimized for TV apps - implementation(libs.androidx.tv.material) - // Material components for mobile implementation(libs.androidx.compose.material3) diff --git a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/components/WatchNowButton.kt b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/components/WatchNowButton.kt index ac5a3ea..0e82101 100644 --- a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/components/WatchNowButton.kt +++ b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/components/WatchNowButton.kt @@ -27,10 +27,16 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.google.jetstream.R +import com.google.jetstream.presentation.components.feature.FormFactor +import com.google.jetstream.presentation.components.feature.rememberFormFactor import com.google.jetstream.presentation.theme.JetStreamButtonShape @Composable @@ -39,9 +45,18 @@ fun WatchNowButton( interactionSource: MutableInteractionSource? = null, onClick: () -> Unit = {}, ) { + val focusRequester = remember { FocusRequester() } + val formFactor = rememberFormFactor() + + LaunchedEffect(Unit) { + if (formFactor == FormFactor.Tv) { + focusRequester.requestFocus() + } + } + Button( onClick = onClick, - modifier = modifier, + modifier = modifier.focusRequester(focusRequester), shape = JetStreamButtonShape, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.onSurface, diff --git a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/DragDetector.kt b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/DragDetector.kt deleted file mode 100644 index dda0d58..0000000 --- a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/DragDetector.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.jetstream.presentation.screens.home - -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -@Composable -internal fun Modifier.dragDetector( - state: FeaturedMoviesCarouselState, - dragDetector: DragDetector = - rememberDragDetector( - moveToPrevious = state::moveToPreviousItem, - moveToNext = state::moveToNextItem - ), -): Modifier = - pointerInput(Unit) { - detectDragGestures { change, dragAmount -> - change.consume() - dragDetector.update(dragAmount.x) - } - } - -internal data class DragDetector( - val threshold: Float, - val moveToNext: () -> Unit, - val moveToPrevious: () -> Unit, -) { - private var delta = 0f - - fun update(delta: Float) { - this.delta += delta - if (this.delta > threshold) { - moveToPrevious() - reset() - } else if (this.delta < -threshold) { - moveToNext() - reset() - } - } - - private fun reset() { - delta = 0f - } -} - -@Composable -internal fun rememberDragDetector( - moveToNext: () -> Unit = {}, - moveToPrevious: () -> Unit = {}, - threshold: Dp = 300.dp, - density: Density = LocalDensity.current, -): DragDetector = - remember(threshold, density) { - val thresholdInPx = - with(density) { - threshold.toPx() - } - DragDetector(thresholdInPx, moveToNext, moveToPrevious) - } diff --git a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarousel.kt b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarousel.kt index 2a48e1d..cfca327 100644 --- a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarousel.kt +++ b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarousel.kt @@ -17,156 +17,199 @@ package com.google.jetstream.presentation.screens.home import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.FocusInteraction -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.Text +import androidx.compose.material3.carousel.HorizontalCenteredHeroCarousel import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import androidx.tv.material3.Carousel -import androidx.tv.material3.CarouselDefaults -import androidx.tv.material3.ExperimentalTvMaterial3Api import coil.compose.AsyncImage import com.google.jetstream.data.entities.Movie import com.google.jetstream.data.util.StringConstants import com.google.jetstream.presentation.components.WatchNowButton import com.google.jetstream.presentation.components.feature.FormFactor -import com.google.jetstream.presentation.components.feature.rememberUiMode -import com.google.jetstream.presentation.theme.Padding -import com.google.jetstream.presentation.theme.jetStreamBorderIndication +import com.google.jetstream.presentation.components.feature.rememberFormFactor +import kotlinx.coroutines.launch -@OptIn(ExperimentalTvMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun FeaturedMoviesCarousel( movies: List, - padding: Padding, goToVideoPlayer: (movie: Movie) -> Unit, modifier: Modifier = Modifier, ) { - val uiMode = rememberUiMode() - val featuredMoviesCarouselState = - rememberSaveable(movies, uiMode, saver = FeaturedMoviesCarouselState.Saver) { - FeaturedMoviesCarouselState( - itemCount = movies.size, - initialWatchNowButtonVisibility = uiMode.formFactor != FormFactor.Tv - ) - } + val watchNow = remember { FocusRequester() } + val bringIntoViewRequester = remember { BringIntoViewRequester() } + val coroutineScope = rememberCoroutineScope() + val formFactor = rememberFormFactor() + val featuredMoviesCarouselState = rememberFeaturedMoviesCarouselState( + movies = movies, + initialWatchNowButtonVisibility = formFactor != FormFactor.Tv + ) + Box( + modifier = modifier, + contentAlignment = Alignment.BottomEnd + ) { + HorizontalCenteredHeroCarousel( + state = featuredMoviesCarouselState.carouselState, + minSmallItemWidth = 0.dp, + maxSmallItemWidth = 0.dp, + modifier = Modifier + .onPreviewKeyEvent { keyEvent -> + when { + keyEvent.type == KeyEventType.KeyDown && + keyEvent.key == Key.DirectionRight -> { + featuredMoviesCarouselState.nextItem() + true + } - val interactionSource = remember { MutableInteractionSource() } - if (uiMode.formFactor == FormFactor.Tv) { - LaunchedEffect(Unit) { - interactionSource.interactions.collect { - when (it) { - is FocusInteraction.Unfocus -> { - featuredMoviesCarouselState.updateWatchNowButtonVisibility(false) - } + keyEvent.type == KeyEventType.KeyDown && + keyEvent.key == Key.DirectionLeft -> { + featuredMoviesCarouselState.previousItem() + true + } - is FocusInteraction.Focus -> { - featuredMoviesCarouselState.updateWatchNowButtonVisibility(true) + else -> false } - - else -> {} } - } - } - } + .bringIntoViewRequester(bringIntoViewRequester) + .onFocusChanged { + when { + it.hasFocus -> { + coroutineScope.launch { + bringIntoViewRequester.bringIntoView() + } + } + } + } + .then( + if (formFactor == FormFactor.Tv) { + Modifier + .onFocusChanged { + if (it.hasFocus) { + featuredMoviesCarouselState + .updateWatchNowButtonVisibility(true) + } else { + featuredMoviesCarouselState + .updateWatchNowButtonVisibility(false) + } + } + .focusable() + } else { + Modifier.focusGroup() + } + ) - Carousel( - modifier = modifier - .padding(start = padding.start, end = padding.start, top = padding.top) - .semantics { - contentDescription = - StringConstants.Composable.ContentDescription.MoviesCarousel - } - .clickable(interactionSource, jetStreamBorderIndication) { - goToVideoPlayer(movies[featuredMoviesCarouselState.activeItemIndex]) - } - .clip(ShapeDefaults.Medium) - .dragDetector(featuredMoviesCarouselState), - itemCount = movies.size, - carouselState = featuredMoviesCarouselState.carouselState, - carouselIndicator = { - CarouselIndicator( - itemCount = movies.size, - activeItemIndex = featuredMoviesCarouselState.activeItemIndex - ) - }, - contentTransformStartToEnd = fadeIn(tween(durationMillis = 1000)) - .togetherWith(fadeOut(tween(durationMillis = 1000))), - contentTransformEndToStart = fadeIn(tween(durationMillis = 1000)) - .togetherWith(fadeOut(tween(durationMillis = 1000))), - content = { index -> - val movie = movies[index] + ) { currentItemIndex -> + val movie = movies[currentItemIndex] // background CarouselItemBackground(movie = movie, modifier = Modifier.fillMaxSize()) // foreground CarouselItemForeground( movie = movie, watchNowButtonVisibility = featuredMoviesCarouselState.watchNowButtonVisibility, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .focusRequester(watchNow), onClick = { - goToVideoPlayer(movies[featuredMoviesCarouselState.activeItemIndex]) + goToVideoPlayer(movies[featuredMoviesCarouselState.currentItem]) } ) } - ) + CarouselIndication( + itemCount = movies.size, + activeItemIndex = featuredMoviesCarouselState.currentItem, + modifier = Modifier.padding(32.dp) + ) + } } -@OptIn(ExperimentalTvMaterial3Api::class) @Composable -private fun BoxScope.CarouselIndicator( +private fun CarouselIndication( itemCount: Int, activeItemIndex: Int, modifier: Modifier = Modifier, ) { Box( modifier = modifier - .padding(32.dp) .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) - .graphicsLayer { - clip = true - shape = ShapeDefaults.ExtraSmall - } - .align(Alignment.BottomEnd) ) { - CarouselDefaults.IndicatorRow( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(8.dp), + IndicatorRow( itemCount = itemCount, - activeItemIndex = activeItemIndex + activeItemIndex = activeItemIndex, + modifier = Modifier + .padding(16.dp) + .graphicsLayer { + clip = true + shape = ShapeDefaults.ExtraSmall + } ) } } +@Composable +private fun IndicatorRow( + itemCount: Int, + activeItemIndex: Int, + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(8.dp), + verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, + indicator: @Composable (isActive: Boolean) -> Unit = { isActive -> + val activeColor = Color.White + val inactiveColor = activeColor.copy(alpha = 0.3f) + + val backgroundColor = if (isActive) activeColor else inactiveColor + Box( + modifier = Modifier + .size(8.dp) + .background(color = backgroundColor, shape = CircleShape) + ) + } +) { + Row( + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, + modifier = modifier + ) { + repeat(itemCount) { + indicator(it == activeItemIndex) + } + } +} + @Composable private fun CarouselItemForeground( movie: Movie, @@ -179,10 +222,7 @@ private fun CarouselItemForeground( contentAlignment = Alignment.BottomStart ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(32.dp), - verticalArrangement = Arrangement.Bottom + modifier = Modifier.padding(32.dp), ) { Text( text = movie.name, diff --git a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarouselState.kt b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarouselState.kt index 7b4d483..f74bf50 100644 --- a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarouselState.kt +++ b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarouselState.kt @@ -17,43 +17,95 @@ package com.google.jetstream.presentation.screens.home import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.carousel.CarouselState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.tv.material3.CarouselState -import androidx.tv.material3.ExperimentalTvMaterial3Api +import com.google.jetstream.data.entities.Movie +import com.google.jetstream.presentation.components.feature.rememberFormFactor +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.parcelize.Parcelize -@OptIn(ExperimentalTvMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) internal class FeaturedMoviesCarouselState( internal val itemCount: Int, initialActiveIndex: Int = 0, initialWatchNowButtonVisibility: Boolean = false, + private val autoScrollDelay: Long = 10000L ) { - var carouselState by mutableStateOf(CarouselState(initialActiveIndex)) - private set + val carouselState = CarouselState(initialActiveIndex) { + itemCount + } - val activeItemIndex: Int - get() = carouselState.activeItemIndex + val currentItem: Int + get() = carouselState.currentItem var watchNowButtonVisibility by mutableStateOf(initialWatchNowButtonVisibility) private set - fun moveToNextItem() { - val updatedIndex = (activeItemIndex + 1) % itemCount - carouselState = CarouselState(updatedIndex) + private val carouselStateUpdateRequestQueue = + MutableStateFlow(CarouselStateUpdateRequest.None) + + fun nextItem() { + carouselStateUpdateRequestQueue + .tryEmit(CarouselStateUpdateRequest.ImmediateScrollToNextItem) + } + + fun previousItem() { + carouselStateUpdateRequestQueue + .tryEmit(CarouselStateUpdateRequest.ImmediateScrollToPreviousItem) } - fun moveToPreviousItem() { - val updatedIndex = (activeItemIndex - 1 + itemCount) % itemCount - carouselState = CarouselState(updatedIndex) + private suspend fun scrollToNextItem() { + val updatedIndex = (currentItem + 1) % itemCount + carouselState.animateScrollToItem(updatedIndex) + } + + private suspend fun scrollToPreviousItem() { + val updatedIndex = (currentItem - 1 + itemCount) % itemCount + carouselState.animateScrollToItem(updatedIndex) } fun updateWatchNowButtonVisibility(isVisible: Boolean) { watchNowButtonVisibility = isVisible } + internal suspend fun startAutoScroll() { + carouselStateUpdateRequestQueue.collectLatest { request -> + when (request) { + CarouselStateUpdateRequest.ImmediateScrollToNextItem -> { + scrollToNextItem() + carouselStateUpdateRequestQueue + .tryEmit(CarouselStateUpdateRequest.ScrollToNextItem) + } + + CarouselStateUpdateRequest.ImmediateScrollToPreviousItem -> { + scrollToPreviousItem() + carouselStateUpdateRequestQueue + .tryEmit(CarouselStateUpdateRequest.ScrollToNextItem) + } + + CarouselStateUpdateRequest.ScrollToNextItem -> { + delay(autoScrollDelay) + carouselStateUpdateRequestQueue + .tryEmit(CarouselStateUpdateRequest.ImmediateScrollToNextItem) + } + + CarouselStateUpdateRequest.None -> { + carouselStateUpdateRequestQueue + .tryEmit(CarouselStateUpdateRequest.ScrollToNextItem) + } + } + } + } + companion object { val Saver = Saver( @@ -63,6 +115,13 @@ internal class FeaturedMoviesCarouselState( } } +private enum class CarouselStateUpdateRequest { + ImmediateScrollToNextItem, + ImmediateScrollToPreviousItem, + ScrollToNextItem, + None, +} + @Parcelize internal data class FeaturedMoviesCarouselSnapshot( val itemCount: Int, @@ -80,8 +139,30 @@ internal data class FeaturedMoviesCarouselSnapshot( fun from(state: FeaturedMoviesCarouselState) = FeaturedMoviesCarouselSnapshot( itemCount = state.itemCount, - activeItemIndex = state.activeItemIndex, + activeItemIndex = state.currentItem, watchButtonVisibility = state.watchNowButtonVisibility ) } } + +@Composable +internal fun rememberFeaturedMoviesCarouselState( + movies: List, + initialWatchNowButtonVisibility: Boolean = true, +): FeaturedMoviesCarouselState { + val formFactor = rememberFormFactor() + return rememberSaveable( + movies, + formFactor, + saver = FeaturedMoviesCarouselState.Saver + ) { + FeaturedMoviesCarouselState( + itemCount = movies.size, + initialWatchNowButtonVisibility = initialWatchNowButtonVisibility + ) + }.also { + LaunchedEffect(it) { + it.startAutoScroll() + } + } +} diff --git a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreen.kt b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreen.kt index dd83839..35ee5c0 100644 --- a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreen.kt +++ b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreen.kt @@ -122,9 +122,9 @@ private fun Catalog( item(contentType = "FeaturedMoviesCarousel") { FeaturedMoviesCarousel( movies = featuredMovies, - padding = contentPadding, goToVideoPlayer = goToVideoPlayer, modifier = Modifier + .padding(contentPadding.intoPaddingValues()) .fillMaxWidth() .height(LocalFeaturedCarouselHeight.current) .focusRequester(carousel) diff --git a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt index e959a13..b66dc2d 100644 --- a/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt +++ b/AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt @@ -49,8 +49,8 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.PlayerSurface import androidx.media3.ui.compose.modifiers.resizeWithContentScale import androidx.xr.compose.platform.LocalSpatialCapabilities +import androidx.xr.compose.spatial.ContentEdge import androidx.xr.compose.spatial.Orbiter -import androidx.xr.compose.spatial.OrbiterEdge import com.google.jetstream.R import com.google.jetstream.data.entities.MovieDetails import com.google.jetstream.presentation.components.BackButton @@ -299,8 +299,7 @@ private fun SpatialVideoPlayerControls( videoPlayerState.isControlsVisible ) { Orbiter( - position = OrbiterEdge.Bottom, - offset = 140.dp + position = ContentEdge.Bottom, ) { Box( modifier = Modifier