diff --git a/JetStreamCompose/benchmark/build.gradle.kts b/JetStreamCompose/benchmark/build.gradle.kts index 2bc216e7..cdec970e 100644 --- a/JetStreamCompose/benchmark/build.gradle.kts +++ b/JetStreamCompose/benchmark/build.gradle.kts @@ -18,6 +18,7 @@ plugins { alias(libs.plugins.android.test) alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) } android { @@ -51,7 +52,6 @@ android { } targetProjectPath = ":jetstream" - experimentalProperties["android.experimental.self-instrumenting"] = true } dependencies { diff --git a/JetStreamCompose/benchmark/src/main/java/com/google/jetstream/benchmark/BaselineProfileGenerator.kt b/JetStreamCompose/benchmark/src/main/java/com/google/jetstream/benchmark/BaselineProfileGenerator.kt index 682a366f..71b1b61a 100644 --- a/JetStreamCompose/benchmark/src/main/java/com/google/jetstream/benchmark/BaselineProfileGenerator.kt +++ b/JetStreamCompose/benchmark/src/main/java/com/google/jetstream/benchmark/BaselineProfileGenerator.kt @@ -69,7 +69,6 @@ class BaselineProfileGenerator { waitForIdle() repeat(2) { pressDPadRight(); waitForIdle() } - // Navigate to Movies tab pressDPadUp() waitForIdle() @@ -127,7 +126,6 @@ class BaselineProfileGenerator { pressDPadRight() waitForIdle() - // Navigate to Search tab repeat(3) { pressDPadUp(); waitForIdle() } pressDPadRight() @@ -188,7 +186,6 @@ class BaselineProfileGenerator { } } - private const val JETSTREAM_PACKAGE_NAME = "com.google.jetstream" private const val INITIAL_WAIT_TIMEOUT = 2000L diff --git a/JetStreamCompose/benchmark/src/main/java/com/google/jetstream/benchmark/StartupBenchmark.kt b/JetStreamCompose/benchmark/src/main/java/com/google/jetstream/benchmark/StartupBenchmark.kt index 8cadda3a..16d576c9 100644 --- a/JetStreamCompose/benchmark/src/main/java/com/google/jetstream/benchmark/StartupBenchmark.kt +++ b/JetStreamCompose/benchmark/src/main/java/com/google/jetstream/benchmark/StartupBenchmark.kt @@ -71,4 +71,4 @@ class StartupBenchmark { } private const val JETSTREAM_PACKAGE_NAME = "com.google.jetstream" -private const val STARTUP_TEST_ITERATIONS = 5 \ No newline at end of file +private const val STARTUP_TEST_ITERATIONS = 5 diff --git a/JetStreamCompose/build.gradle.kts b/JetStreamCompose/build.gradle.kts index 50e44ac3..e2c1f86f 100644 --- a/JetStreamCompose/build.gradle.kts +++ b/JetStreamCompose/build.gradle.kts @@ -23,10 +23,11 @@ https://youtrack.jetbrains.com/issue/KTIJ-19369/ */ plugins { - alias(libs.plugins.android.application) apply(false) - alias(libs.plugins.kotlin.android) apply(false) - alias(libs.plugins.kotlin.serialization) apply(false) - alias(libs.plugins.android.test) apply(false) - alias(libs.plugins.hilt) apply(false) + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.android.test) apply false + alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false } diff --git a/JetStreamCompose/buildscripts/init.gradle.kts b/JetStreamCompose/buildscripts/init.gradle.kts new file mode 100644 index 00000000..1b7a5426 --- /dev/null +++ b/JetStreamCompose/buildscripts/init.gradle.kts @@ -0,0 +1,72 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * 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. + */ + +val ktlintVersion = "0.46.1" + +initscript { + val spotlessVersion = "6.10.0" + + repositories { + mavenCentral() + } + + dependencies { + classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion") + } +} + +allprojects { + if (this == rootProject) { + return@allprojects + } + apply() + extensions.configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktlint(ktlintVersion).editorConfigOverride( + mapOf( + "ktlint_code_style" to "android", + "ij_kotlin_allow_trailing_comma" to true, + // These rules were introduced in ktlint 0.46.0 and should not be + // enabled without further discussion. They are disabled for now. + // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 + "disabled_rules" to + "filename," + + "annotation,annotation-spacing," + + "argument-list-wrapping," + + "double-colon-spacing," + + "enum-entry-name-case," + + "multiline-if-else," + + "no-empty-first-line-in-method-block," + + "package-name," + + "trailing-comma," + + "spacing-around-angle-brackets," + + "spacing-between-declarations-with-annotations," + + "spacing-between-declarations-with-comments," + + "unary-op-spacing" + ) + ) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + format("kts") { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + } +} \ No newline at end of file diff --git a/JetStreamCompose/gradle/libs.versions.toml b/JetStreamCompose/gradle/libs.versions.toml index 1c485555..2d135c0c 100644 --- a/JetStreamCompose/gradle/libs.versions.toml +++ b/JetStreamCompose/gradle/libs.versions.toml @@ -1,20 +1,22 @@ [versions] activity-compose = "1.9.0" -android-gradle-plugin = "8.3.2" -android-test-plugin = "8.3.2" +android-gradle-plugin = "8.4.0" +android-test-plugin = "8.4.0" benchmark-macro-junit4 = "1.2.4" coil-compose = "2.6.0" -compose-bom = "2024.04.01" -compose-for-tv = "1.0.0-alpha10" -core-ktx = "1.13.0" +compose-bom = "2024.05.00" +compose-foundation = "1.7.0-beta01" +compose-ui = "1.7.0-beta01" +tv-material = "1.0.0-beta01" +core-ktx = "1.13.1" core-splashscreen = "1.0.1" hilt-navigation-compose = "1.2.0" -hilt-android = "2.51" +hilt-android = "2.51.1" junit = "1.1.5" -kotlin-android = "1.9.0" +kotlin-android = "2.0.0-RC3" kotlinx-serialization = "1.6.0" -ksp = "1.9.0-1.0.13" -lifecycle-runtime-ktx = "2.7.0" +ksp = "2.0.0-RC3-1.0.20" +lifecycle-runtime-ktx = "2.8.0" media3-ui = "1.3.1" media3-exoplayer = "1.3.1" navigation-compose = "2.7.7" @@ -25,9 +27,10 @@ uiautomator = "2.3.0" androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmark-macro-junit4" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } -androidx-compose-ui-base = { module = "androidx.compose.ui:ui" } -androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } -androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-foundation-base = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation"} +androidx-compose-ui-base = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-ui" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose-ui" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "core-splashscreen" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" } @@ -40,8 +43,7 @@ androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "medi androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3-exoplayer" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" } -androidx-tv-foundation = { module = "androidx.tv:tv-foundation", version.ref = "compose-for-tv" } -androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "compose-for-tv" } +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" } @@ -53,5 +55,6 @@ android-application = { id = "com.android.application", version.ref = "android-g android-test = { id = "com.android.test", version.ref = "android-test-plugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-android" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-android" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-android" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt-android" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file diff --git a/JetStreamCompose/gradle/wrapper/gradle-wrapper.properties b/JetStreamCompose/gradle/wrapper/gradle-wrapper.properties index 3499ded5..509c4a29 100644 --- a/JetStreamCompose/gradle/wrapper/gradle-wrapper.properties +++ b/JetStreamCompose/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/JetStreamCompose/jetstream/build.gradle.kts b/JetStreamCompose/jetstream/build.gradle.kts index d5771b75..624f98e5 100644 --- a/JetStreamCompose/jetstream/build.gradle.kts +++ b/JetStreamCompose/jetstream/build.gradle.kts @@ -19,6 +19,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.compiler) alias(libs.plugins.hilt) alias(libs.plugins.ksp) } @@ -27,6 +28,10 @@ kotlin { jvmToolchain(17) } +composeCompiler { + enableStrongSkippingMode = true +} + android { namespace = "com.google.jetstream" // Needed for latest androidx snapshot build @@ -69,9 +74,7 @@ android { compose = true buildConfig = true } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.0" - } + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" @@ -90,11 +93,13 @@ dependencies { implementation(libs.androidx.compose.ui.base) implementation(libs.androidx.compose.ui.tooling.preview) + // Compose foundation library to replace tv-foundation + implementation(libs.androidx.compose.foundation.base) + // extra material icons implementation(libs.androidx.material.icons.extended) - // TV Compose - implementation(libs.androidx.tv.foundation) + // Material components optimized for TV apps implementation(libs.androidx.tv.material) // ViewModel in Compose diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/JetStreamApplication.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/JetStreamApplication.kt index d88d5b8c..07e3b157 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/JetStreamApplication.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/JetStreamApplication.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -36,5 +36,4 @@ abstract class MovieRepositoryModule { abstract fun bindMovieRepository( movieRepositoryImpl: MovieRepositoryImpl ): MovieRepository - } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/MainActivity.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/MainActivity.kt index 871388c4..41f18b58 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/MainActivity.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/MainActivity.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2024 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 import android.os.Bundle @@ -40,4 +56,3 @@ class MainActivity : ComponentActivity() { } } } - diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/Movie.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/Movie.kt index 2c15c937..633937fc 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/Movie.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/Movie.kt @@ -45,4 +45,4 @@ fun MoviesResponseItem.toMovie(thumbnailType: ThumbnailType = ThumbnailType.Stan enum class ThumbnailType { Standard, Long -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieCategoryList.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieCategoryList.kt index 9a38ee24..cc89391c 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieCategoryList.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieCategoryList.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,6 +18,7 @@ package com.google.jetstream.data.entities import androidx.compose.runtime.Immutable +@Deprecated("This data class is deprecated as strong skip mode is enabled.") @Immutable data class MovieCategoryList( val value: List = emptyList() diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieDetails.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieDetails.kt index e6b72f32..84a9d80a 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieDetails.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieDetails.kt @@ -35,6 +35,6 @@ data class MovieDetails( val originalLanguage: String, val budget: String, val revenue: String, - val similarMovies: List, + val similarMovies: MovieList, val reviewsAndRatings: List ) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieList.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieList.kt index 09e03de9..d3d75232 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieList.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieList.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,7 +18,8 @@ package com.google.jetstream.data.entities import androidx.compose.runtime.Immutable +@Deprecated("This data class is deprecated as strong skip mode is enabled.") @Immutable data class MovieList( val value: List = emptyList() -) : List by value \ No newline at end of file +) : List by value diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/models/MovieCategoriesResponse.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/models/MovieCategoriesResponse.kt index dd36854a..a25561f2 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/models/MovieCategoriesResponse.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/models/MovieCategoriesResponse.kt @@ -18,7 +18,6 @@ package com.google.jetstream.data.models import kotlinx.serialization.Serializable - @Serializable data class MovieCategoriesResponseItem( val id: String, diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/CachedDataReader.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/CachedDataReader.kt index 3f1b097f..6a42b1bd 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/CachedDataReader.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/CachedDataReader.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -72,4 +72,4 @@ internal suspend fun readMovieCategoryData( assetsReader.getJsonDataFromAsset(resourceId).map { Json.decodeFromString>(it) }.getOrDefault(emptyList()) -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieCastDataSource.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieCastDataSource.kt index 6f79e808..79701ab4 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieCastDataSource.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieCastDataSource.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -32,6 +32,4 @@ class MovieCastDataSource @Inject constructor( } suspend fun getMovieCastList() = movieCastDataReader.read() - - -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieCategoryDataSource.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieCategoryDataSource.kt index c7739635..64e806f1 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieCategoryDataSource.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieCategoryDataSource.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -32,5 +32,4 @@ class MovieCategoryDataSource @Inject constructor( } suspend fun getMovieCategoryList() = movieCategoryDataReader.read() - -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieDataSource.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieDataSource.kt index c942eda2..2931084d 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieDataSource.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieDataSource.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -71,7 +71,6 @@ class MovieDataSource @Inject constructor( suspend fun getTop10MovieList() = movieWithLongThumbnailDataReader.read().subList(20, 30) - suspend fun getNowPlayingMovieList() = nowPlayingMovieDataReader.read() @@ -80,5 +79,4 @@ class MovieDataSource @Inject constructor( suspend fun getFavoriteMovieList() = movieDataReader.read().subList(0, 28) - } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepository.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepository.kt index 758d42ca..589a6681 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepository.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepository.kt @@ -38,4 +38,3 @@ interface MovieRepository { fun getBingeWatchDramas(): Flow fun getFavouriteMovies(): Flow } - diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt index 040a791c..96090129 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt @@ -27,10 +27,10 @@ import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.DefaultCoun import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.DefaultRating import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.FreshTomatoes import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.ReviewerName -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow @Singleton class MovieRepositoryImpl @Inject constructor( @@ -157,6 +157,4 @@ class MovieRepositoryImpl @Inject constructor( val list = movieDataSource.getFavoriteMovieList() emit(MovieList(list)) } - } - diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/TvDataSource.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/TvDataSource.kt index a964c5af..1945d91b 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/TvDataSource.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/TvDataSource.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -36,5 +36,4 @@ class TvDataSource @Inject constructor( suspend fun getBingeWatchDramaList() = mostPopularTvShowsReader.read().subList(6, 15).map { it.toMovie() } - -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/AssetReader.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/AssetReader.kt index f0b94602..efede54c 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/AssetReader.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/AssetReader.kt @@ -32,5 +32,4 @@ class AssetsReader @Inject constructor( Result.failure(e) } } - } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/StringConstants.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/StringConstants.kt index 19e7c620..70df94c0 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/StringConstants.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/StringConstants.kt @@ -71,16 +71,16 @@ object StringConstants { object Placeholders { const val AboutSectionTitle = "About JetStream" const val AboutSectionDescription = "Welcome to Jetstream! We are a new and" + - " exciting streaming platform that offers a vast selection of movies," + - " TV shows, and original content for you to enjoy. Our team is dedicated" + - " to providing an intuitive and seamless streaming experience for all" + - " users. With a simple and intuitive interface, you can easily find and" + - " watch your favourite content in just a few clicks. We are constantly" + - " updating and expanding our library, so there is always something new" + - " to discover. We also offer personalised recommendations based on your" + - " viewing history, so you can easily find new and exciting content to" + - " enjoy. Thank you for choosing Jetstream for all of your entertainment" + - " needs. We hope you have a great time streaming!" + " exciting streaming platform that offers a vast selection of movies," + + " TV shows, and original content for you to enjoy. Our team is dedicated" + + " to providing an intuitive and seamless streaming experience for all" + + " users. With a simple and intuitive interface, you can easily find and" + + " watch your favourite content in just a few clicks. We are constantly" + + " updating and expanding our library, so there is always something new" + + " to discover. We also offer personalised recommendations based on your" + + " viewing history, so you can easily find new and exciting content to" + + " enjoy. Thank you for choosing Jetstream for all of your entertainment" + + " needs. We hope you have a great time streaming!" const val AboutSectionAppVersionTitle = "Application Version" const val LanguageSectionTitle = "Language" val LanguageSectionItems = listOf( diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/App.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/App.kt index 7f3397ef..af6abcec 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/App.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/App.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/Error.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/Error.kt new file mode 100644 index 00000000..01933a10 --- /dev/null +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/Error.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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.common + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.tv.material3.Text +import com.google.jetstream.R + +@Composable +fun Error(modifier: Modifier = Modifier) { + Text( + text = stringResource(id = R.string.message_error), + modifier = modifier + ) +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/Loading.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/Loading.kt new file mode 100644 index 00000000..7504b40a --- /dev/null +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/Loading.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import com.google.jetstream.R + +@Composable +fun Loading( + modifier: Modifier = Modifier, + style: TextStyle = MaterialTheme.typography.displayMedium +) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Text( + text = stringResource(id = R.string.message_loading), + style = style + ) + } +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/MovieCard.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/MovieCard.kt new file mode 100644 index 00000000..054e99c1 --- /dev/null +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/MovieCard.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024 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.common + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.tv.material3.Border +import androidx.tv.material3.ClickableSurfaceDefaults +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.StandardCardContainer +import androidx.tv.material3.Surface +import com.google.jetstream.presentation.theme.JetStreamBorderWidth +import com.google.jetstream.presentation.theme.JetStreamCardShape + +@Composable +fun MovieCard( + onClick: () -> Unit, + modifier: Modifier = Modifier, + title: @Composable () -> Unit = {}, + image: @Composable BoxScope.() -> Unit, +) { + StandardCardContainer( + modifier = modifier, + title = title, + imageCard = { + Surface( + onClick = onClick, + shape = ClickableSurfaceDefaults.shape(JetStreamCardShape), + border = ClickableSurfaceDefaults.border( + focusedBorder = Border( + border = BorderStroke( + width = JetStreamBorderWidth, + color = MaterialTheme.colorScheme.onSurface + ), + shape = JetStreamCardShape + ) + ), + scale = ClickableSurfaceDefaults.scale(focusedScale = 1f), + content = image + ) + }, + ) +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/MoviesRow.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/MoviesRow.kt index 304fcc73..ea1dd48c 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/MoviesRow.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/MoviesRow.kt @@ -18,7 +18,6 @@ package com.google.jetstream.presentation.common import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -27,9 +26,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -45,8 +45,6 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -54,23 +52,11 @@ 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 androidx.tv.foundation.PivotOffsets -import androidx.tv.foundation.lazy.list.TvLazyRow -import androidx.tv.foundation.lazy.list.itemsIndexed -import androidx.tv.material3.Border -import androidx.tv.material3.CardDefaults -import androidx.tv.material3.CardLayoutDefaults -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.ImmersiveListScope import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.StandardCardLayout import androidx.tv.material3.Text -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.google.jetstream.data.entities.Movie +import com.google.jetstream.data.entities.MovieList import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding -import com.google.jetstream.presentation.theme.JetStreamBorderWidth -import com.google.jetstream.presentation.theme.JetStreamCardShape import com.google.jetstream.presentation.utils.createInitialFocusRestorerModifiers import com.google.jetstream.presentation.utils.ifElse @@ -79,9 +65,9 @@ enum class ItemDirection(val aspectRatio: Float) { Horizontal(16f / 9f); } -@OptIn(ExperimentalTvMaterial3Api::class) @Composable fun MoviesRow( + movieList: MovieList, modifier: Modifier = Modifier, itemDirection: ItemDirection = ItemDirection.Vertical, startPadding: Dp = rememberChildPadding().start, @@ -93,34 +79,29 @@ fun MoviesRow( ), showItemTitle: Boolean = true, showIndexOverImage: Boolean = false, - focusedItemIndex: (index: Int) -> Unit = {}, - movies: List, - onMovieClick: (movie: Movie) -> Unit = {} + onMovieSelected: (movie: Movie) -> Unit = {} ) { Column( modifier = modifier.focusGroup() ) { - title?.let { nnTitle -> + if (title != null) { Text( - text = nnTitle, + text = title, style = titleStyle, modifier = Modifier .alpha(1f) - .padding(start = startPadding) - .padding(vertical = 16.dp) + .padding(start = startPadding, top = 16.dp, bottom = 16.dp) ) } - AnimatedContent( - targetState = movies, + targetState = movieList, label = "", ) { movieState -> val focusRestorerModifiers = createInitialFocusRestorerModifiers() - TvLazyRow( + LazyRow( modifier = Modifier .then(focusRestorerModifiers.parentModifier), - pivotOffsets = PivotOffsets(parentFraction = 0.07f), contentPadding = PaddingValues( start = startPadding, end = endPadding, @@ -135,10 +116,9 @@ fun MoviesRow( focusRestorerModifiers.childModifier ) .weight(1f), - focusedItemIndex = focusedItemIndex, index = index, itemDirection = itemDirection, - onMovieClick = onMovieClick, + onMovieSelected = onMovieSelected, movie = movie, showItemTitle = showItemTitle, showIndexOverImage = showIndexOverImage @@ -149,9 +129,10 @@ fun MoviesRow( } } -@OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class) +@OptIn(ExperimentalComposeUiApi::class) @Composable -fun ImmersiveListScope.ImmersiveListMoviesRow( +fun ImmersiveListMoviesRow( + movieList: MovieList, modifier: Modifier = Modifier, itemDirection: ItemDirection = ItemDirection.Vertical, startPadding: Dp = rememberChildPadding().start, @@ -163,16 +144,15 @@ fun ImmersiveListScope.ImmersiveListMoviesRow( ), showItemTitle: Boolean = true, showIndexOverImage: Boolean = false, - focusedItemIndex: (index: Int) -> Unit = {}, - movies: List, - onMovieClick: (movie: Movie) -> Unit = {} + onMovieSelected: (Movie) -> Unit = {}, + onMovieFocused: (Movie) -> Unit = {} ) { Column( modifier = modifier.focusGroup() ) { - title?.let { nnTitle -> + if (title != null) { Text( - text = nnTitle, + text = title, style = titleStyle, modifier = Modifier .alpha(1f) @@ -180,34 +160,31 @@ fun ImmersiveListScope.ImmersiveListMoviesRow( .padding(vertical = 16.dp) ) } - AnimatedContent( - targetState = movies, + targetState = movieList, label = "", ) { movieState -> - TvLazyRow( + LazyRow( modifier = Modifier.focusRestorer(), - pivotOffsets = PivotOffsets(parentFraction = 0.07f), contentPadding = PaddingValues(start = startPadding, end = endPadding), horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - movieState.forEachIndexed { index, movie -> - item { - key(movie.id) { - MoviesRowItem( - modifier = Modifier - .weight(1f) - .immersiveListItem(index), - focusedItemIndex = focusedItemIndex, - index = index, - itemDirection = itemDirection, - onMovieClick = onMovieClick, - movie = movie, - showItemTitle = showItemTitle, - showIndexOverImage = showIndexOverImage - ) - } + itemsIndexed( + movieState, + key = { _, movie -> + movie.id } + ) { index, movie -> + MoviesRowItem( + modifier = Modifier.weight(1f), + index = index, + itemDirection = itemDirection, + onMovieSelected = onMovieSelected, + onMovieFocused = onMovieFocused, + movie = movie, + showItemTitle = showItemTitle, + showIndexOverImage = showIndexOverImage + ) } } } @@ -215,77 +192,62 @@ fun ImmersiveListScope.ImmersiveListMoviesRow( } @Composable -@OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class) private fun MoviesRowItem( - modifier: Modifier = Modifier, - focusedItemIndex: (index: Int) -> Unit, index: Int, - itemDirection: ItemDirection, - onMovieClick: (movie: Movie) -> Unit, movie: Movie, + onMovieSelected: (Movie) -> Unit, showItemTitle: Boolean, - showIndexOverImage: Boolean + showIndexOverImage: Boolean, + modifier: Modifier = Modifier, + itemDirection: ItemDirection = ItemDirection.Vertical, + onMovieFocused: (Movie) -> Unit = {}, ) { - var isItemFocused by remember { mutableStateOf(false) } + var isFocused by remember { mutableStateOf(false) } - StandardCardLayout( - modifier = Modifier - .onFocusChanged { - isItemFocused = it.isFocused - if (isItemFocused) { - focusedItemIndex(index) - } - } - .focusProperties { - if (index == 0) { - left = FocusRequester.Cancel - } - } - .then(modifier), + MovieCard( + onClick = { onMovieSelected(movie) }, title = { MoviesRowItemText( showItemTitle = showItemTitle, - isItemFocused = isItemFocused, + isItemFocused = isFocused, movie = movie ) }, - imageCard = { - CardLayoutDefaults.ImageCard( - onClick = { onMovieClick(movie) }, - shape = CardDefaults.shape(JetStreamCardShape), - border = CardDefaults.border( - focusedBorder = Border( - border = BorderStroke( - width = JetStreamBorderWidth, - color = MaterialTheme.colorScheme.onSurface - ), - shape = JetStreamCardShape - ) - ), - scale = CardDefaults.scale(focusedScale = 1f), - interactionSource = it - ) { - MoviesRowItemImage( - modifier = Modifier.aspectRatio(itemDirection.aspectRatio), - showIndexOverImage = showIndexOverImage, - movie = movie, - index = index - ) + modifier = Modifier + .onFocusChanged { + isFocused = it.isFocused + if (it.isFocused) { + onMovieFocused(movie) + } } - }, - ) + .focusProperties { + left = if (index == 0) { + FocusRequester.Cancel + } else { + FocusRequester.Default + } + } + .then(modifier) + ) { + MoviesRowItemImage( + modifier = Modifier.aspectRatio(itemDirection.aspectRatio), + showIndexOverImage = showIndexOverImage, + movie = movie, + index = index + ) + } } -@OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun MoviesRowItemImage( - showIndexOverImage: Boolean, movie: Movie, + showIndexOverImage: Boolean, index: Int, modifier: Modifier = Modifier, ) { Box(contentAlignment = Alignment.CenterStart) { - AsyncImage( + PosterImage( + movie = movie, modifier = modifier .fillMaxWidth() .drawWithContent { @@ -298,12 +260,6 @@ private fun MoviesRowItemImage( ) } }, - model = ImageRequest.Builder(LocalContext.current) - .crossfade(true) - .data(movie.posterUri) - .build(), - contentDescription = "movie poster of ${movie.name}", - contentScale = ContentScale.Crop ) if (showIndexOverImage) { Text( @@ -323,7 +279,6 @@ private fun MoviesRowItemImage( } } -@OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun MoviesRowItemText( showItemTitle: Boolean, diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/PosterImage.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/PosterImage.kt new file mode 100644 index 00000000..1ed85980 --- /dev/null +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/common/PosterImage.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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.common + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.google.jetstream.data.entities.Movie +import com.google.jetstream.data.util.StringConstants + +@Composable +fun PosterImage( + movie: Movie, + modifier: Modifier = Modifier, +) { + AsyncImage( + modifier = modifier, + model = ImageRequest.Builder(LocalContext.current) + .crossfade(true) + .data(movie.posterUri) + .build(), + contentDescription = StringConstants.Composable.ContentDescription.moviePoster(movie.name), + contentScale = ContentScale.Crop + ) +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoriesScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoriesScreen.kt index 2ba7e7c2..31708dc2 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoriesScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoriesScreen.kt @@ -18,11 +18,14 @@ package com.google.jetstream.presentation.screens.categories import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -41,21 +44,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.tv.foundation.lazy.grid.TvGridCells -import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid -import androidx.tv.foundation.lazy.grid.itemsIndexed -import androidx.tv.foundation.lazy.grid.rememberTvLazyGridState -import androidx.tv.material3.Border -import androidx.tv.material3.CardDefaults -import androidx.tv.material3.CardLayoutDefaults -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.StandardCardLayout import androidx.tv.material3.Text import com.google.jetstream.data.entities.MovieCategoryList +import com.google.jetstream.presentation.common.Loading +import com.google.jetstream.presentation.common.MovieCard import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding -import com.google.jetstream.presentation.theme.JetStreamBorderWidth -import com.google.jetstream.presentation.theme.JetStreamCardShape import com.google.jetstream.presentation.utils.GradientBg @Composable @@ -69,9 +63,10 @@ fun CategoriesScreen( val uiState by categoriesScreenViewModel.uiState.collectAsStateWithLifecycle() when (val s = uiState) { - is CategoriesScreenUiState.Loading -> { - Loading() + CategoriesScreenUiState.Loading -> { + Loading(modifier = Modifier.fillMaxSize()) } + is CategoriesScreenUiState.Ready -> { Catalog( gridColumns = gridColumns, @@ -82,10 +77,9 @@ fun CategoriesScreen( ) } } - } -@OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class) +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun Catalog( movieCategories: MovieCategoryList, @@ -95,11 +89,11 @@ private fun Catalog( onScroll: (isTopBarVisible: Boolean) -> Unit, ) { val childPadding = rememberChildPadding() - val tvLazyGridState = rememberTvLazyGridState() + val lazyGridState = rememberLazyGridState() val shouldShowTopBar by remember { derivedStateOf { - tvLazyGridState.firstVisibleItemIndex == 0 && - tvLazyGridState.firstVisibleItemScrollOffset < 100 + lazyGridState.firstVisibleItemIndex == 0 && + lazyGridState.firstVisibleItemScrollOffset < 100 } } LaunchedEffect(shouldShowTopBar) { @@ -113,77 +107,48 @@ private fun Catalog( .padding(top = childPadding.top), label = "", ) { it -> - TvLazyVerticalGrid( - state = tvLazyGridState, + LazyVerticalGrid( + state = lazyGridState, modifier = modifier, - columns = TvGridCells.Fixed(gridColumns), - content = { - itemsIndexed(it) { index, movieCategory -> - var isFocused by remember { mutableStateOf(false) } - StandardCardLayout( - imageCard = { - CardLayoutDefaults.ImageCard( - shape = CardDefaults.shape(shape = JetStreamCardShape), - border = CardDefaults.border( - focusedBorder = Border( - border = BorderStroke( - width = JetStreamBorderWidth, - color = MaterialTheme.colorScheme.onSurface - ), - shape = JetStreamCardShape - ), - pressedBorder = Border( - border = BorderStroke( - width = JetStreamBorderWidth, - color = MaterialTheme.colorScheme.border - ), - shape = JetStreamCardShape - ) - ), - scale = CardDefaults.scale(focusedScale = 1f), - onClick = { onCategoryClick(movieCategory.id) }, - interactionSource = it - ) { - val itemAlpha by animateFloatAsState( - targetValue = if (isFocused) .6f else 0.2f, - label = "" - ) - val textColor = if (isFocused) Color.White else Color.White - - Box(contentAlignment = Alignment.Center) { - Box(modifier = Modifier.alpha(itemAlpha)) { - GradientBg() - } - Text( - text = movieCategory.name, - style = MaterialTheme.typography.titleMedium.copy( - color = textColor, - ) - ) - } - } - }, - modifier = Modifier - .padding(8.dp) - .aspectRatio(16 / 9f) - .onFocusChanged { - isFocused = it.isFocused || it.hasFocus + columns = GridCells.Fixed(gridColumns), + ) { + itemsIndexed(it) { index, movieCategory -> + var isFocused by remember { mutableStateOf(false) } + MovieCard( + onClick = { + onCategoryClick(movieCategory.id) + }, + modifier = Modifier + .padding(8.dp) + .aspectRatio(16 / 9f) + .onFocusChanged { + isFocused = it.isFocused || it.hasFocus + } + .focusProperties { + if (index % gridColumns == 0) { + left = FocusRequester.Cancel } - .focusProperties { - if (index % gridColumns == 0) { - left = FocusRequester.Cancel - } - }, - title = {} + } + ) { + val itemAlpha by animateFloatAsState( + targetValue = if (isFocused) .6f else 0.2f, + label = "" ) + val textColor = if (isFocused) Color.White else Color.White + + Box(contentAlignment = Alignment.Center) { + Box(modifier = Modifier.alpha(itemAlpha)) { + GradientBg() + } + Text( + text = movieCategory.name, + style = MaterialTheme.typography.titleMedium.copy( + color = textColor, + ) + ) + } } } - ) + } } } - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Loading(modifier: Modifier = Modifier) { - Text(text = "Loading...", modifier = modifier) -} \ No newline at end of file diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoriesScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoriesScreenViewModel.kt index eadfd8f0..3cafc957 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoriesScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoriesScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -21,15 +21,15 @@ import androidx.lifecycle.viewModelScope import com.google.jetstream.data.entities.MovieCategoryList import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class CategoriesScreenViewModel @Inject constructor( movieRepository: MovieRepository -): ViewModel() { +) : ViewModel() { val uiState = movieRepository.getMovieCategories().map { CategoriesScreenUiState.Ready(categoryList = it) @@ -38,12 +38,10 @@ class CategoriesScreenViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000), initialValue = CategoriesScreenUiState.Loading ) - } sealed interface CategoriesScreenUiState { - object Loading: CategoriesScreenUiState - data class Ready(val categoryList: MovieCategoryList): CategoriesScreenUiState - -} \ No newline at end of file + data object Loading : CategoriesScreenUiState + data class Ready(val categoryList: MovieCategoryList) : CategoriesScreenUiState +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoryMovieListScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoryMovieListScreen.kt index 5a6b02a7..4d5f5f67 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoryMovieListScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoryMovieListScreen.kt @@ -17,43 +17,34 @@ package com.google.jetstream.presentation.screens.categories import androidx.activity.compose.BackHandler -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.tv.foundation.lazy.grid.TvGridCells -import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid -import androidx.tv.material3.Border -import androidx.tv.material3.CardDefaults -import androidx.tv.material3.CardLayoutDefaults -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.StandardCardLayout import androidx.tv.material3.Text -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.google.jetstream.data.entities.Movie import com.google.jetstream.data.entities.MovieCategoryDetails -import com.google.jetstream.data.util.StringConstants +import com.google.jetstream.presentation.common.Error +import com.google.jetstream.presentation.common.Loading +import com.google.jetstream.presentation.common.MovieCard +import com.google.jetstream.presentation.common.PosterImage import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding -import com.google.jetstream.presentation.theme.JetStreamBorderWidth import com.google.jetstream.presentation.theme.JetStreamBottomListPadding -import com.google.jetstream.presentation.theme.JetStreamCardShape import com.google.jetstream.presentation.utils.focusOnInitialVisibility object CategoryMovieListScreen { @@ -69,12 +60,12 @@ fun CategoryMovieListScreen( val uiState by categoryMovieListScreenViewModel.uiState.collectAsStateWithLifecycle() when (val s = uiState) { - is CategoryMovieListScreenUiState.Loading -> { - Loading() + CategoryMovieListScreenUiState.Loading -> { + Loading(modifier = Modifier.fillMaxSize()) } - is CategoryMovieListScreenUiState.Error -> { - Error() + CategoryMovieListScreenUiState.Error -> { + Error(modifier = Modifier.fillMaxSize()) } is CategoryMovieListScreenUiState.Done -> { @@ -86,10 +77,8 @@ fun CategoryMovieListScreen( ) } } - } -@OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun CategoryDetails( categoryDetails: MovieCategoryDetails, @@ -115,76 +104,30 @@ private fun CategoryDetails( vertical = childPadding.top.times(3.5f) ) ) - TvLazyVerticalGrid( - columns = TvGridCells.Fixed(6), + LazyVerticalGrid( + columns = GridCells.Fixed(6), contentPadding = PaddingValues(bottom = JetStreamBottomListPadding) ) { - categoryDetails.movies.forEachIndexed { index, movie -> - item { - key(movie.id) { - StandardCardLayout( - modifier = Modifier - .aspectRatio(1 / 1.5f) - .padding(8.dp) - .then( - if (index == 0) - Modifier.focusOnInitialVisibility(isFirstItemVisible) - else Modifier - ), - imageCard = { - CardLayoutDefaults.ImageCard( - shape = CardDefaults.shape(shape = JetStreamCardShape), - border = CardDefaults.border( - focusedBorder = Border( - border = BorderStroke( - width = JetStreamBorderWidth, - color = MaterialTheme.colorScheme.onSurface - ), - shape = JetStreamCardShape - ), - pressedBorder = Border( - border = BorderStroke( - width = JetStreamBorderWidth, - color = MaterialTheme.colorScheme.border - ), - shape = JetStreamCardShape - ), - ), - scale = CardDefaults.scale(focusedScale = 1f), - onClick = { onMovieSelected(movie) }, - interactionSource = it - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(movie.posterUri) - .crossfade(true) - .build(), - contentDescription = StringConstants - .Composable - .ContentDescription - .moviePoster(movie.name), - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } - }, - title = {}, - ) - } + itemsIndexed( + categoryDetails.movies, + key = { _, movie -> + movie.id + } + ) { index, movie -> + MovieCard( + onClick = { onMovieSelected(movie) }, + modifier = Modifier + .aspectRatio(1 / 1.5f) + .padding(8.dp) + .then( + if (index == 0) + Modifier.focusOnInitialVisibility(isFirstItemVisible) + else Modifier + ), + ) { + PosterImage(movie = movie, modifier = Modifier.fillMaxSize()) } } } } } - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Loading(modifier: Modifier = Modifier) { - Text(text = "Loading...", modifier = modifier) -} - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Error(modifier: Modifier = Modifier) { - Text(text = "Wops, something went wrong...", modifier = modifier) -} \ No newline at end of file diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoryMovieListScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoryMovieListScreenViewModel.kt index 27e5c414..81725403 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoryMovieListScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/categories/CategoryMovieListScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -22,10 +22,10 @@ import androidx.lifecycle.viewModelScope import com.google.jetstream.data.entities.MovieCategoryDetails import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class CategoryMovieListScreenViewModel @Inject constructor( @@ -49,11 +49,10 @@ class CategoryMovieListScreenViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000), initialValue = CategoryMovieListScreenUiState.Loading ) - } sealed interface CategoryMovieListScreenUiState { object Loading : CategoryMovieListScreenUiState object Error : CategoryMovieListScreenUiState data class Done(val movieCategoryDetails: MovieCategoryDetails) : CategoryMovieListScreenUiState -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/dashboard/DashboardTopBar.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/dashboard/DashboardTopBar.kt index 463f6a44..0169e346 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/dashboard/DashboardTopBar.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/dashboard/DashboardTopBar.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.LocalContentColor import androidx.tv.material3.MaterialTheme @@ -63,14 +62,14 @@ import com.google.jetstream.presentation.theme.JetStreamCardShape import com.google.jetstream.presentation.theme.LexendExa import com.google.jetstream.presentation.utils.occupyScreenSize -val TopBarTabs = Screens.values().toList().filter { it.isTabItem } +val TopBarTabs = Screens.entries.toList().filter { it.isTabItem } // +1 for ProfileTab val TopBarFocusRequesters = List(size = TopBarTabs.size + 1) { FocusRequester() } private const val PROFILE_SCREEN_INDEX = -1 -@OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class) +@OptIn(ExperimentalComposeUiApi::class) @Composable fun DashboardTopBar( modifier: Modifier = Modifier, @@ -91,6 +90,7 @@ fun DashboardTopBar( ) { UserAvatar( modifier = Modifier + .size(32.dp) .focusRequester(focusRequesters[0]) .semantics { contentDescription = @@ -159,27 +159,36 @@ fun DashboardTopBar( } } Spacer(modifier = Modifier.weight(1f)) - Row( + JetStreamLogo( modifier = Modifier .alpha(0.75f) .padding(end = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Default.PlayCircle, - contentDescription = StringConstants.Composable - .ContentDescription.BrandLogoImage, - modifier = Modifier - .padding(end = 4.dp) - .size(IconSize) - ) - Text( - text = stringResource(R.string.brand_logo_text), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Medium, - fontFamily = LexendExa - ) - } + ) } } } + +@Composable +private fun JetStreamLogo( + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.PlayCircle, + contentDescription = StringConstants.Composable + .ContentDescription.BrandLogoImage, + modifier = Modifier + .padding(end = 4.dp) + .size(IconSize) + ) + Text( + text = stringResource(R.string.brand_logo_text), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Medium, + fontFamily = LexendExa + ) + } +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/dashboard/UserAvatar.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/dashboard/UserAvatar.kt index bd23c194..fa454c42 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/dashboard/UserAvatar.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/dashboard/UserAvatar.kt @@ -18,22 +18,18 @@ package com.google.jetstream.presentation.screens.dashboard import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.tv.material3.Border -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.SelectableSurfaceDefaults import androidx.tv.material3.Surface -import androidx.tv.material3.ToggleableSurfaceDefaults import com.google.jetstream.presentation.theme.JetStreamBorderWidth -@OptIn(ExperimentalTvMaterial3Api::class) @Composable fun UserAvatar( selected: Boolean, @@ -41,8 +37,10 @@ fun UserAvatar( onClick: () -> Unit ) { Surface( - shape = ToggleableSurfaceDefaults.shape(shape = CircleShape), - border = ToggleableSurfaceDefaults.border( + selected = selected, + onClick = onClick, + shape = SelectableSurfaceDefaults.shape(shape = CircleShape), + border = SelectableSurfaceDefaults.border( focusedBorder = Border( border = BorderStroke( width = JetStreamBorderWidth, @@ -58,10 +56,8 @@ fun UserAvatar( shape = CircleShape ), ), - scale = ToggleableSurfaceDefaults.scale(focusedScale = 1f), - modifier = modifier.size(32.dp), - checked = selected, - onCheckedChange = { onClick() } + scale = SelectableSurfaceDefaults.scale(focusedScale = 1f), + modifier = modifier, ) { Icon( imageVector = Icons.Default.AccountCircle, diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FavouriteScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FavouriteScreenViewModel.kt index f0825f1b..2b4c5572 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FavouriteScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FavouriteScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -24,19 +24,18 @@ import com.google.jetstream.R import com.google.jetstream.data.entities.MovieList import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class FavouriteScreenViewModel @Inject constructor( movieRepository: MovieRepository ) : ViewModel() { - private val selectedFilterList = MutableStateFlow(FilterList()) val uiState: StateFlow = combine( @@ -74,7 +73,6 @@ sealed interface FavouriteScreenUiState { data object Loading : FavouriteScreenUiState data class Ready(val favouriteMovieList: MovieList, val selectedFilterList: FilterList) : FavouriteScreenUiState - } @Immutable @@ -98,4 +96,4 @@ enum class FilterCondition(val idList: List, @StringRes val labelId: Int) { TvShows((10..17).toList(), R.string.favorites_tv_shows), AddedLastWeek((18..23).toList(), R.string.favorites_added_last_week), AvailableIn4K((24..28).toList(), R.string.favorites_available_in_4k), -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FavouritesScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FavouritesScreen.kt index 3bb87022..8eb21f53 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FavouritesScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FavouritesScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -30,10 +31,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.tv.foundation.lazy.grid.rememberTvLazyGridState -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.Text import com.google.jetstream.data.entities.MovieList +import com.google.jetstream.presentation.common.Loading import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding @Composable @@ -46,7 +45,7 @@ fun FavouritesScreen( val uiState by favouriteScreenViewModel.uiState.collectAsStateWithLifecycle() when (val s = uiState) { is FavouriteScreenUiState.Loading -> { - Loading() + Loading(modifier = Modifier.fillMaxSize()) } is FavouriteScreenUiState.Ready -> { Catalog( @@ -63,12 +62,6 @@ fun FavouritesScreen( } } -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Loading(modifier: Modifier = Modifier) { - Text(text = "Loading...", modifier = modifier) -} - @Composable private fun Catalog( favouriteMovieList: MovieList, @@ -81,11 +74,11 @@ private fun Catalog( modifier: Modifier = Modifier, ) { val childPadding = rememberChildPadding() - val filteredMoviesGridState = rememberTvLazyGridState() + val filteredMoviesGridState = rememberLazyGridState() val shouldShowTopBar by remember { derivedStateOf { filteredMoviesGridState.firstVisibleItemIndex == 0 && - filteredMoviesGridState.firstVisibleItemScrollOffset < 100 + filteredMoviesGridState.firstVisibleItemScrollOffset < 100 } } @@ -104,17 +97,16 @@ private fun Catalog( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.padding(horizontal = childPadding.start) ) { - MovieFilterChipRow( - filterList = filterList, - selectedFilterList = selectedFilterList, - modifier = Modifier.padding(top = chipRowTopPadding), - onSelectedFilterListUpdated = onSelectedFilterListUpdated - ) - FilteredMoviesGrid( - state = filteredMoviesGridState, - movieList = favouriteMovieList, - onMovieClick = onMovieClick - ) - } - + MovieFilterChipRow( + filterList = filterList, + selectedFilterList = selectedFilterList, + modifier = Modifier.padding(top = chipRowTopPadding), + onSelectedFilterListUpdated = onSelectedFilterListUpdated + ) + FilteredMoviesGrid( + state = filteredMoviesGridState, + movieList = favouriteMovieList, + onMovieClick = onMovieClick + ) + } } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FilteredMoviesGrid.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FilteredMoviesGrid.kt index 9c43f8e8..62fc9b4a 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FilteredMoviesGrid.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/FilteredMoviesGrid.kt @@ -16,83 +16,43 @@ package com.google.jetstream.presentation.screens.favourites -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.tv.foundation.lazy.grid.TvGridCells -import androidx.tv.foundation.lazy.grid.TvLazyGridState -import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid -import androidx.tv.foundation.lazy.grid.items -import androidx.tv.material3.Border -import androidx.tv.material3.CardDefaults -import androidx.tv.material3.CardLayoutDefaults -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.StandardCardLayout -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.google.jetstream.data.entities.MovieList -import com.google.jetstream.data.util.StringConstants +import com.google.jetstream.presentation.common.MovieCard +import com.google.jetstream.presentation.common.PosterImage import com.google.jetstream.presentation.theme.JetStreamBottomListPadding -import com.google.jetstream.presentation.theme.JetStreamCardShape -@OptIn(ExperimentalTvMaterial3Api::class) @Composable fun FilteredMoviesGrid( - state: TvLazyGridState, + state: LazyGridState, movieList: MovieList, onMovieClick: (movieId: String) -> Unit, ) { - TvLazyVerticalGrid( + LazyVerticalGrid( state = state, modifier = Modifier.fillMaxSize(), - columns = TvGridCells.Fixed(6), + columns = GridCells.Fixed(6), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(bottom = JetStreamBottomListPadding), ) { items(movieList, key = { it.id }) { movie -> - StandardCardLayout( + MovieCard( + onClick = { onMovieClick(movie.id) }, modifier = Modifier.aspectRatio(1 / 1.5f), - imageCard = { - CardLayoutDefaults.ImageCard( - onClick = { onMovieClick(movie.id) }, - shape = CardDefaults.shape(shape = JetStreamCardShape), - scale = CardDefaults.scale(focusedScale = 1f), - border = CardDefaults.border( - focusedBorder = Border( - border = BorderStroke( - width = 2.dp, - color = MaterialTheme.colorScheme.onSurface - ), - shape = JetStreamCardShape - ) - ), - interactionSource = it - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(movie.posterUri) - .crossfade(true) - .build(), - contentDescription = StringConstants - .Composable - .ContentDescription - .moviePoster(movie.name), - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } - }, - title = {} - ) + ) { + PosterImage(movie = movie, modifier = Modifier.fillMaxSize()) + } } } } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/MovieFilterChip.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/MovieFilterChip.kt index d63d5758..c28ba40d 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/MovieFilterChip.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/MovieFilterChip.kt @@ -75,7 +75,8 @@ fun MovieFilterChip( AnimatedVisibility(visible = isChecked) { Icon( Icons.Default.Check, - contentDescription = StringConstants.Composable.ContentDescription.FilterSelected, + contentDescription = + StringConstants.Composable.ContentDescription.FilterSelected, modifier = Modifier.size(16.dp) ) } @@ -85,7 +86,8 @@ fun MovieFilterChip( border = Border( border = BorderStroke( width = 1.dp, color = MaterialTheme.colorScheme.border.copy(alpha = 0.5f) - ), shape = JetStreamCardShape + ), + shape = JetStreamCardShape ), focusedBorder = ChipFocusedBorder, ), @@ -115,5 +117,6 @@ private val ChipFocusedBorder border = BorderStroke( width = 1.5.dp, color = MaterialTheme.colorScheme.onSurface, - ), shape = JetStreamCardShape + ), + shape = JetStreamCardShape ) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/MovieFilterChipRow.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/MovieFilterChipRow.kt index 3f62ecfa..38f502a2 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/MovieFilterChipRow.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/favourites/MovieFilterChipRow.kt @@ -61,4 +61,4 @@ fun MovieFilterChipRow( ) } } -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarousel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarousel.kt index 8bb187a0..d85467b0 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarousel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/FeaturedMoviesCarousel.kt @@ -92,13 +92,18 @@ fun FeaturedMoviesCarousel( ) { val carouselState = rememberSaveable(saver = CarouselSaver) { CarouselState(0) } var isCarouselFocused by remember { mutableStateOf(false) } + val alpha = if (isCarouselFocused) { + 1f + } else { + 0f + } Carousel( modifier = modifier .padding(start = padding.start, end = padding.start, top = padding.top) .border( width = JetStreamBorderWidth, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (isCarouselFocused) 1f else 0f), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha), shape = ShapeDefaults.Medium, ) .clip(ShapeDefaults.Medium) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreen.kt index 9641ced1..79a909c5 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -33,14 +35,11 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.tv.foundation.PivotOffsets -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.rememberTvLazyListState -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.Text import com.google.jetstream.data.entities.Movie import com.google.jetstream.data.entities.MovieList import com.google.jetstream.data.util.StringConstants +import com.google.jetstream.presentation.common.Error +import com.google.jetstream.presentation.common.Loading import com.google.jetstream.presentation.common.MoviesRow import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding @@ -69,10 +68,9 @@ fun HomeScreen( ) } - is HomeScreenUiState.Loading -> Loading() - is HomeScreenUiState.Error -> Error() + is HomeScreenUiState.Loading -> Loading(modifier = Modifier.fillMaxSize()) + is HomeScreenUiState.Error -> Error(modifier = Modifier.fillMaxSize()) } - } @Composable @@ -88,16 +86,14 @@ private fun Catalog( isTopBarVisible: Boolean = true, ) { - val tvLazyListState = rememberTvLazyListState() + val lazyListState = rememberLazyListState() val childPadding = rememberChildPadding() - val pivotOffset = remember { PivotOffsets() } - val pivotOffsetForImmersiveList = remember { PivotOffsets(0f, 0f) } var immersiveListHasFocus by remember { mutableStateOf(false) } val shouldShowTopBar by remember { derivedStateOf { - tvLazyListState.firstVisibleItemIndex == 0 && - tvLazyListState.firstVisibleItemScrollOffset < 300 + lazyListState.firstVisibleItemIndex == 0 && + lazyListState.firstVisibleItemScrollOffset < 300 } } @@ -105,16 +101,16 @@ private fun Catalog( onScroll(shouldShowTopBar) } LaunchedEffect(isTopBarVisible) { - if (isTopBarVisible) tvLazyListState.animateScrollToItem(0) + if (isTopBarVisible) lazyListState.animateScrollToItem(0) } - TvLazyColumn( - modifier = modifier, - pivotOffsets = if (immersiveListHasFocus) pivotOffsetForImmersiveList else pivotOffset, - state = tvLazyListState, - contentPadding = PaddingValues(bottom = 108.dp) + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(bottom = 108.dp), // Setting overscan margin to bottom to ensure the last row's visibility + modifier = modifier, ) { + item(contentType = "FeaturedMoviesCarousel") { FeaturedMoviesCarousel( movies = featuredMovies, @@ -132,39 +128,27 @@ private fun Catalog( item(contentType = "MoviesRow") { MoviesRow( modifier = Modifier.padding(top = 16.dp), - movies = trendingMovies, + movieList = trendingMovies, title = StringConstants.Composable.HomeScreenTrendingTitle, - onMovieClick = onMovieClick + onMovieSelected = onMovieClick ) } item(contentType = "Top10MoviesList") { Top10MoviesList( + movieList = top10Movies, + onMovieClick = onMovieClick, modifier = Modifier.onFocusChanged { immersiveListHasFocus = it.hasFocus }, - moviesState = top10Movies, - onMovieClick = onMovieClick ) } item(contentType = "MoviesRow") { MoviesRow( modifier = Modifier.padding(top = 16.dp), - movies = nowPlayingMovies, + movieList = nowPlayingMovies, title = StringConstants.Composable.HomeScreenNowPlayingMoviesTitle, - onMovieClick = onMovieClick + onMovieSelected = onMovieClick ) } } } - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Loading(modifier: Modifier = Modifier) { - Text(text = "Loading...", modifier = modifier) -} - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Error(modifier: Modifier = Modifier) { - Text(text = "Wops, something went wrong.", modifier = modifier) -} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreenViewModel.kt index 9d660336..fe29a7ac 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/HomeScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -21,14 +21,14 @@ import androidx.lifecycle.viewModelScope import com.google.jetstream.data.entities.MovieList import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel -class HomeScreeViewModel @Inject constructor(movieRepository: MovieRepository): ViewModel() { +class HomeScreeViewModel @Inject constructor(movieRepository: MovieRepository) : ViewModel() { val uiState: StateFlow = combine( movieRepository.getFeaturedMovies(), @@ -36,23 +36,26 @@ class HomeScreeViewModel @Inject constructor(movieRepository: MovieRepository): movieRepository.getTop10Movies(), movieRepository.getNowPlayingMovies(), ) { featuredMovieList, trendingMovieList, top10MovieList, nowPlayingMovieList -> - HomeScreenUiState.Ready(featuredMovieList, trendingMovieList, top10MovieList, nowPlayingMovieList) + HomeScreenUiState.Ready( + featuredMovieList, + trendingMovieList, + top10MovieList, + nowPlayingMovieList + ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = HomeScreenUiState.Loading ) - } sealed interface HomeScreenUiState { - data object Loading: HomeScreenUiState - data object Error: HomeScreenUiState + data object Loading : HomeScreenUiState + data object Error : HomeScreenUiState data class Ready( val featuredMovieList: MovieList, val trendingMovieList: MovieList, val top10MovieList: MovieList, val nowPlayingMovieList: MovieList - ): HomeScreenUiState - -} \ No newline at end of file + ) : HomeScreenUiState +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/Top10MoviesList.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/Top10MoviesList.kt index 5dd1dd8f..b26bf442 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/Top10MoviesList.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/home/Top10MoviesList.kt @@ -16,13 +16,17 @@ package com.google.jetstream.presentation.screens.home +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -31,150 +35,189 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.focus.FocusState +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.layout.ContentScale -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInWindow -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.ImmersiveList import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.google.jetstream.R import com.google.jetstream.data.entities.Movie +import com.google.jetstream.data.entities.MovieList import com.google.jetstream.presentation.common.ImmersiveListMoviesRow import com.google.jetstream.presentation.common.ItemDirection +import com.google.jetstream.presentation.common.PosterImage import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding +import com.google.jetstream.presentation.utils.bringIntoViewIfChildrenAreFocused -@OptIn(ExperimentalTvMaterial3Api::class) @Composable fun Top10MoviesList( + movieList: MovieList, modifier: Modifier = Modifier, - moviesState: List, + gradientColor: Color = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f), onMovieClick: (movie: Movie) -> Unit ) { - var currentItemIndex by remember { mutableStateOf(0) } var isListFocused by remember { mutableStateOf(false) } - var currentYCoord: Float? by remember { mutableStateOf(null) } + var selectedMovie by remember(movieList) { mutableStateOf(movieList.first()) } + + val sectionTitle = if (isListFocused) { + null + } else { + stringResource(R.string.top_10_movies_title) + } ImmersiveList( - modifier = modifier.onGloballyPositioned { currentYCoord = it.positionInWindow().y }, - background = { _, listHasFocus -> - isListFocused = listHasFocus - val gradientColor = MaterialTheme.colorScheme.surface - AnimatedVisibility( - visible = isListFocused, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - modifier = Modifier - .height(432.dp) - .gradientOverlay(gradientColor) - ) { - val movie = remember(moviesState, currentItemIndex) { - moviesState[currentItemIndex] - } + selectedMovie = selectedMovie, + isListFocused = isListFocused, + gradientColor = gradientColor, + movieList = movieList, + sectionTitle = sectionTitle, + onMovieClick = onMovieClick, + onMovieFocused = { + selectedMovie = it + }, + onFocusChanged = { + isListFocused = it.hasFocus + }, + modifier = modifier.bringIntoViewIfChildrenAreFocused( + PaddingValues(bottom = 116.dp) + ) + ) +} - Crossfade( - targetState = movie.posterUri, - label = "posterUriCrossfade" - ) { posterUri -> - AsyncImage( - modifier = Modifier - .fillMaxWidth() - .height(432.dp), - model = ImageRequest.Builder(LocalContext.current) - .data(posterUri) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop +@Composable +private fun ImmersiveList( + selectedMovie: Movie, + isListFocused: Boolean, + gradientColor: Color, + movieList: MovieList, + sectionTitle: String?, + onFocusChanged: (FocusState) -> Unit, + onMovieFocused: (Movie) -> Unit, + onMovieClick: (Movie) -> Unit, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.BottomStart, + modifier = modifier + ) { + Background( + movie = selectedMovie, + visible = isListFocused, + modifier = modifier + .height(432.dp) + .gradientOverlay(gradientColor) + ) + Column { + if (isListFocused) { + MovieDescription( + movie = selectedMovie, + modifier = Modifier.padding( + start = rememberChildPadding().start, + bottom = 40.dp ) - } - - } - }, - list = { - Column { - // TODO this causes the whole vertical list to jump - if (isListFocused) { - val movie = remember(moviesState, currentItemIndex) { - moviesState[currentItemIndex] - } - Column( - modifier = Modifier.padding( - start = rememberChildPadding().start, - bottom = 32.dp - ) - ) { - Text(text = movie.name, style = MaterialTheme.typography.displaySmall) - Spacer(modifier = Modifier.padding(top = 8.dp)) - Text( - modifier = Modifier.fillMaxWidth(0.5f), - text = movie.description, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), - fontWeight = FontWeight.Light - ) - } - } - ImmersiveListMoviesRow( - itemDirection = ItemDirection.Horizontal, - movies = moviesState, - title = if (isListFocused) null - else stringResource(R.string.top_10_movies_title), - showItemTitle = !isListFocused, - onMovieClick = onMovieClick, - showIndexOverImage = true, - focusedItemIndex = { focusedIndex -> currentItemIndex = focusedIndex } ) } + + ImmersiveListMoviesRow( + movieList = movieList, + itemDirection = ItemDirection.Horizontal, + title = sectionTitle, + showItemTitle = !isListFocused, + showIndexOverImage = true, + onMovieSelected = onMovieClick, + onMovieFocused = onMovieFocused, + modifier = Modifier.onFocusChanged(onFocusChanged) + ) } - ) + } } -fun Modifier.gradientOverlay(gradientColor: Color) = this then drawWithCache { - val horizontalGradient = Brush.horizontalGradient( - colors = listOf( - gradientColor, - Color.Transparent - ), - startX = size.width.times(0.2f), - endX = size.width.times(0.7f) - ) - val verticalGradient = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - gradientColor - ), - endY = size.width.times(0.3f) - ) - val linearGradient = Brush.linearGradient( - colors = listOf( - gradientColor, - Color.Transparent - ), - start = Offset( - size.width.times(0.2f), - size.height.times(0.5f) - ), - end = Offset( - size.width.times(0.9f), - 0f - ) - ) +@Composable +private fun Background( + movie: Movie, + visible: Boolean, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = visible, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + modifier = modifier + ) { + Crossfade( + targetState = movie, + label = "posterUriCrossfade", - onDrawWithContent { - drawContent() - drawRect(horizontalGradient) - drawRect(verticalGradient) - drawRect(linearGradient) + ) { + PosterImage(movie = it, modifier = Modifier.fillMaxSize()) + } } } + +@Composable +private fun MovieDescription( + movie: Movie, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = movie.name, style = MaterialTheme.typography.displaySmall) + Text( + modifier = Modifier.fillMaxWidth(0.5f), + text = movie.description, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), + fontWeight = FontWeight.Light + ) + } +} + +private fun Modifier.gradientOverlay(gradientColor: Color): Modifier = + drawWithCache { + val horizontalGradient = Brush.horizontalGradient( + colors = listOf( + gradientColor, + Color.Transparent + ), + startX = size.width.times(0.2f), + endX = size.width.times(0.7f) + ) + val verticalGradient = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + gradientColor + ), + endY = size.width.times(0.3f) + ) + val linearGradient = Brush.linearGradient( + colors = listOf( + gradientColor, + Color.Transparent + ), + start = Offset( + size.width.times(0.2f), + size.height.times(0.5f) + ), + end = Offset( + size.width.times(0.9f), + 0f + ) + ) + + onDrawWithContent { + drawContent() + drawRect(horizontalGradient) + drawRect(verticalGradient) + drawRect(linearGradient) + } + } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/CastAndCrewList.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/CastAndCrewList.kt index a318645a..600422bd 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/CastAndCrewList.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/CastAndCrewList.kt @@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -35,9 +37,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.tv.foundation.PivotOffsets -import androidx.tv.foundation.lazy.list.TvLazyRow -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.Border import androidx.tv.material3.CardDefaults import androidx.tv.material3.ClassicCard @@ -66,13 +65,11 @@ fun CastAndCrewList(castAndCrew: List) { ), modifier = Modifier.padding(start = childPadding.start) ) - TvLazyRow( + // ToDo: specify the pivot offset + LazyRow( modifier = Modifier .padding(top = 16.dp) .focusRestorer(), - pivotOffsets = PivotOffsets( - parentFraction = 0.07f - ), contentPadding = PaddingValues(start = childPadding.start) ) { items(castAndCrew, key = { it.id }) { diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetails.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetails.kt index aa350864..5466d819 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetails.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetails.kt @@ -17,7 +17,6 @@ package com.google.jetstream.presentation.screens.movies import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -36,11 +35,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.drawWithContent +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.input.key.NativeKeyEvent -import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -49,7 +48,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.tv.material3.Button import androidx.tv.material3.ButtonDefaults -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -76,10 +74,11 @@ fun MovieDetails( modifier = Modifier .fillMaxWidth() .height(432.dp) + .bringIntoViewRequester(bringIntoViewRequester) ) { MovieImageWithGradients( movieDetails = movieDetails, - bringIntoViewRequester = bringIntoViewRequester + modifier = Modifier.fillMaxSize() ) Column(modifier = Modifier.fillMaxWidth(0.55f)) { @@ -109,11 +108,10 @@ fun MovieDetails( ) } WatchTrailerButton( - modifier = Modifier.onKeyEvent { - if (it.nativeKeyEvent.keyCode == NativeKeyEvent.KEYCODE_DPAD_UP) { + modifier = Modifier.onFocusChanged { + if (it.isFocused) { coroutineScope.launch { bringIntoViewRequester.bringIntoView() } } - false }, goToMoviePlayer = goToMoviePlayer ) @@ -122,8 +120,6 @@ fun MovieDetails( } } - -@OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun WatchTrailerButton( modifier: Modifier = Modifier, @@ -178,7 +174,6 @@ private fun DirectorScreenplayMusicRow( } } -@OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun MovieDescription(description: String) { Text( @@ -192,7 +187,6 @@ private fun MovieDescription(description: String) { ) } -@OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun MovieLargeTitle(movieTitle: String) { Text( @@ -204,11 +198,11 @@ private fun MovieLargeTitle(movieTitle: String) { ) } -@OptIn(ExperimentalFoundationApi::class, ExperimentalTvMaterial3Api::class) @Composable private fun MovieImageWithGradients( movieDetails: MovieDetails, - bringIntoViewRequester: BringIntoViewRequester + modifier: Modifier = Modifier, + gradientColor: Color = MaterialTheme.colorScheme.surface, ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current).data(movieDetails.posterUri) @@ -217,53 +211,29 @@ private fun MovieImageWithGradients( .Composable .ContentDescription .moviePoster(movieDetails.name), - modifier = Modifier - .fillMaxSize() - .bringIntoViewRequester(bringIntoViewRequester), - contentScale = ContentScale.Crop - ) - - Box( - modifier = Modifier - .fillMaxSize() - .background( + contentScale = ContentScale.Crop, + modifier = modifier.drawWithContent { + drawContent() + drawRect( Brush.verticalGradient( - colors = listOf( - Color.Transparent, - MaterialTheme.colorScheme.surface - ), + colors = listOf(Color.Transparent, gradientColor), startY = 600f ) ) - ) - - Box( - modifier = Modifier - .fillMaxSize() - .background( + drawRect( Brush.horizontalGradient( - colors = listOf( - MaterialTheme.colorScheme.surface, - Color.Transparent - ), + colors = listOf(gradientColor, Color.Transparent), endX = 1000f, startX = 300f ) ) - ) - - Box( - modifier = Modifier - .fillMaxSize() - .background( + drawRect( Brush.linearGradient( - colors = listOf( - MaterialTheme.colorScheme.surface, - Color.Transparent - ), + colors = listOf(gradientColor, Color.Transparent), start = Offset(x = 500f, y = 500f), end = Offset(x = 1000f, y = 0f) ) ) + } ) } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetailsScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetailsScreen.kt index 30dbcaa4..9b28c616 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetailsScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetailsScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -36,14 +37,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Text import com.google.jetstream.R import com.google.jetstream.data.entities.Movie import com.google.jetstream.data.entities.MovieDetails import com.google.jetstream.data.util.StringConstants +import com.google.jetstream.presentation.common.Error +import com.google.jetstream.presentation.common.Loading import com.google.jetstream.presentation.common.MoviesRow import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding @@ -62,11 +62,11 @@ fun MovieDetailsScreen( when (val s = uiState) { is MovieDetailsScreenUiState.Loading -> { - Loading() + Loading(modifier = Modifier.fillMaxSize()) } is MovieDetailsScreenUiState.Error -> { - Error() + Error(modifier = Modifier.fillMaxSize()) } is MovieDetailsScreenUiState.Done -> { @@ -83,7 +83,6 @@ fun MovieDetailsScreen( } } -@OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun Details( movieDetails: MovieDetails, @@ -95,7 +94,7 @@ private fun Details( val childPadding = rememberChildPadding() BackHandler(onBack = onBackPressed) - TvLazyColumn( + LazyColumn( contentPadding = PaddingValues(bottom = 135.dp), modifier = modifier, ) { @@ -118,8 +117,8 @@ private fun Details( .Composable .movieDetailsScreenSimilarTo(movieDetails.name), titleStyle = MaterialTheme.typography.titleMedium, - movies = movieDetails.similarMovies, - onMovieClick = refreshScreenWithNewMovie + movieList = movieDetails.similarMovies, + onMovieSelected = refreshScreenWithNewMovie ) } @@ -176,16 +175,4 @@ private fun Details( } } -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Loading(modifier: Modifier = Modifier) { - Text(text = "Loading...", modifier = modifier) -} - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Error(modifier: Modifier = Modifier) { - Text(text = "Something went wrong...", modifier = modifier) -} - -private val BottomDividerPadding = PaddingValues(vertical = 48.dp) \ No newline at end of file +private val BottomDividerPadding = PaddingValues(vertical = 48.dp) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetailsScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetailsScreenViewModel.kt index aff42cb6..aca4a1c3 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetailsScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MovieDetailsScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -22,10 +22,10 @@ import androidx.lifecycle.viewModelScope import com.google.jetstream.data.entities.MovieDetails import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class MovieDetailsScreenViewModel @Inject constructor( @@ -49,7 +49,7 @@ class MovieDetailsScreenViewModel @Inject constructor( } sealed class MovieDetailsScreenUiState { - object Loading : MovieDetailsScreenUiState() - object Error : MovieDetailsScreenUiState() + data object Loading : MovieDetailsScreenUiState() + data object Error : MovieDetailsScreenUiState() data class Done(val movieDetails: MovieDetails) : MovieDetailsScreenUiState() -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreen.kt index 52fcdd14..eb4aae45 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreen.kt @@ -19,6 +19,8 @@ package com.google.jetstream.presentation.screens.movies import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -28,13 +30,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.rememberTvLazyListState -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.Text import com.google.jetstream.data.entities.Movie import com.google.jetstream.data.entities.MovieList import com.google.jetstream.data.util.StringConstants +import com.google.jetstream.presentation.common.Loading import com.google.jetstream.presentation.common.MoviesRow import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding @@ -71,11 +70,11 @@ private fun Catalog( modifier: Modifier = Modifier, ) { val childPadding = rememberChildPadding() - val tvLazyListState = rememberTvLazyListState() + val lazyListState = rememberLazyListState() val shouldShowTopBar by remember { derivedStateOf { - tvLazyListState.firstVisibleItemIndex == 0 && - tvLazyListState.firstVisibleItemScrollOffset == 0 + lazyListState.firstVisibleItemIndex == 0 && + lazyListState.firstVisibleItemScrollOffset == 0 } } @@ -83,12 +82,12 @@ private fun Catalog( onScroll(shouldShowTopBar) } LaunchedEffect(isTopBarVisible) { - if (isTopBarVisible) tvLazyListState.animateScrollToItem(0) + if (isTopBarVisible) lazyListState.animateScrollToItem(0) } - TvLazyColumn( + LazyColumn( modifier = modifier, - state = tvLazyListState, + state = lazyListState, contentPadding = PaddingValues(top = childPadding.top, bottom = 104.dp) ) { item { @@ -101,15 +100,9 @@ private fun Catalog( MoviesRow( modifier = Modifier.padding(top = childPadding.top), title = StringConstants.Composable.PopularFilmsThisWeekTitle, - movies = popularFilmsThisWeek, - onMovieClick = onMovieClick + movieList = popularFilmsThisWeek, + onMovieSelected = onMovieClick ) } } } - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Loading(modifier: Modifier = Modifier) { - Text(text = "Loading...", modifier = modifier) -} \ No newline at end of file diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreenMovieList.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreenMovieList.kt index 10ff1032..fadadafe 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreenMovieList.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreenMovieList.kt @@ -27,6 +27,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -42,9 +44,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.tv.foundation.PivotOffsets -import androidx.tv.foundation.lazy.list.TvLazyRow -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.Border import androidx.tv.material3.CardDefaults import androidx.tv.material3.CompactCard @@ -71,9 +70,9 @@ fun MoviesScreenMovieList( targetState = movieList, label = "", ) { movieListTarget -> - TvLazyRow( + // ToDo: specify the pivot offset to 0.07f + LazyRow( modifier = Modifier.focusRestorer(), - pivotOffsets = PivotOffsets(parentFraction = 0.07f), contentPadding = PaddingValues(start = startPadding, end = endPadding) ) { items(movieListTarget) { @@ -87,7 +86,6 @@ fun MoviesScreenMovieList( } } - @OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun MovieListItem( diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreenViewModel.kt index 30bc2958..13f4bc21 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/movies/MoviesScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -21,10 +21,10 @@ import androidx.lifecycle.viewModelScope import com.google.jetstream.data.entities.MovieList import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class MoviesScreenViewModel @Inject constructor( @@ -35,16 +35,21 @@ class MoviesScreenViewModel @Inject constructor( movieRepository.getMoviesWithLongThumbnail(), movieRepository.getPopularFilmsThisWeek(), ) { (movieList, popularFilmsThisWeek) -> - MoviesScreenUiState.Ready(movieList = movieList, popularFilmsThisWeek = popularFilmsThisWeek) + MoviesScreenUiState.Ready( + movieList = movieList, + popularFilmsThisWeek = popularFilmsThisWeek + ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = MoviesScreenUiState.Loading ) - } sealed interface MoviesScreenUiState { - object Loading: MoviesScreenUiState - data class Ready(val movieList: MovieList, val popularFilmsThisWeek: MovieList): MoviesScreenUiState -} \ No newline at end of file + data object Loading : MoviesScreenUiState + data class Ready( + val movieList: MovieList, + val popularFilmsThisWeek: MovieList + ) : MoviesScreenUiState +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/AccountsSection.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/AccountsSection.kt index dde52b50..c2616eef 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/AccountsSection.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/AccountsSection.kt @@ -19,6 +19,8 @@ package com.google.jetstream.presentation.screens.profile import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue @@ -29,8 +31,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp -import androidx.tv.foundation.lazy.grid.TvGridCells -import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid import com.google.jetstream.data.util.StringConstants import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding @@ -76,11 +76,11 @@ fun AccountsSection() { ) } - TvLazyVerticalGrid( + LazyVerticalGrid( modifier = Modifier .fillMaxSize() .padding(horizontal = childPadding.start), - columns = TvGridCells.Fixed(2), + columns = GridCells.Fixed(2), content = { items(accountsSectionListItems.size) { index -> AccountsSelectionItem( diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/HelpAndSupportSection.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/HelpAndSupportSection.kt index e15090b2..5adf41db 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/HelpAndSupportSection.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/HelpAndSupportSection.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos import androidx.compose.material.icons.filled.ArrowForwardIos import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -53,7 +54,6 @@ fun HelpAndSupportSection() { } } - @OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun HelpAndSupportSectionItem( @@ -72,7 +72,7 @@ private fun HelpAndSupportSectionItem( ) } ?: run { Icon( - Icons.Default.ArrowForwardIos, + Icons.AutoMirrored.Filled.ArrowForwardIos, modifier = Modifier.size(ListItemDefaults.IconSizeDense), contentDescription = StringConstants .Composable diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/LanguageSection.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/LanguageSection.kt index fe3e4736..8c771093 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/LanguageSection.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/LanguageSection.kt @@ -17,14 +17,13 @@ package com.google.jetstream.presentation.screens.profile import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.ListItem import androidx.tv.material3.ListItemDefaults @@ -35,14 +34,13 @@ import com.google.jetstream.R import com.google.jetstream.data.util.StringConstants import com.google.jetstream.presentation.theme.JetStreamCardShape -@OptIn(ExperimentalTvMaterial3Api::class) @Composable fun LanguageSection( selectedIndex: Int, onSelectedIndexChange: (currentIndex: Int) -> Unit ) { with(StringConstants.Composable.Placeholders) { - TvLazyColumn(modifier = Modifier.padding(horizontal = 72.dp)) { + LazyColumn(modifier = Modifier.padding(horizontal = 72.dp)) { item { Text( text = LanguageSectionTitle, @@ -59,7 +57,8 @@ fun LanguageSection( Icon( Icons.Default.Check, contentDescription = stringResource( - id = R.string.language_section_listItem_icon_content_description, + id = + R.string.language_section_listItem_icon_content_description, LanguageSectionItems[index] ) ) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/ProfileScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/ProfileScreen.kt index 9194e0d1..d7047cca 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/ProfileScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/ProfileScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -60,7 +61,6 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.ListItem import androidx.tv.material3.ListItemDefaults @@ -70,10 +70,7 @@ import com.google.jetstream.R import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding import com.google.jetstream.presentation.theme.JetStreamTheme -@OptIn( - ExperimentalComposeUiApi::class, - ExperimentalTvMaterial3Api::class -) +@OptIn(ExperimentalComposeUiApi::class) @Composable fun ProfileScreen( @FloatRange(from = 0.0, to = 1.0) @@ -108,7 +105,7 @@ fun ProfileScreen( .focusGroup(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - ProfileScreens.values().forEachIndexed { index, profileScreen -> + ProfileScreens.entries.forEachIndexed { index, profileScreen -> // TODO: make this dense list item key(index) { ListItem( @@ -165,7 +162,7 @@ fun ProfileScreen( } } - var selectedLanguageIndex by rememberSaveable { mutableStateOf(0) } + var selectedLanguageIndex by rememberSaveable { mutableIntStateOf(0) } var isSubtitlesChecked by rememberSaveable { mutableStateOf(true) } NavHost( modifier = Modifier @@ -213,8 +210,6 @@ fun ProfileScreen( } } - -@OptIn(ExperimentalTvMaterial3Api::class) @Preview(device = Devices.TV_1080p) @Composable fun ProfileScreenPreview() { diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/SearchHistorySection.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/SearchHistorySection.kt index 5ac26506..b9cae976 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/SearchHistorySection.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/profile/SearchHistorySection.kt @@ -20,12 +20,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.material3.Button -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.ListItem import androidx.tv.material3.ListItemDefaults import androidx.tv.material3.MaterialTheme @@ -33,11 +32,10 @@ import androidx.tv.material3.Text import com.google.jetstream.data.util.StringConstants import com.google.jetstream.presentation.theme.JetStreamCardShape -@OptIn(ExperimentalTvMaterial3Api::class) @Composable fun SearchHistorySection() { with(StringConstants.Composable.Placeholders) { - TvLazyColumn(modifier = Modifier.padding(horizontal = 72.dp)) { + LazyColumn(modifier = Modifier.padding(horizontal = 72.dp)) { item { Row( modifier = Modifier.fillMaxWidth(), diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/search/SearchScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/search/SearchScreen.kt index 5d449167..102b8743 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/search/SearchScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/search/SearchScreen.kt @@ -25,6 +25,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -49,9 +52,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.TvLazyListState -import androidx.tv.foundation.lazy.list.rememberTvLazyListState import androidx.tv.material3.Border import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ExperimentalTvMaterial3Api @@ -73,11 +73,11 @@ fun SearchScreen( onScroll: (isTopBarVisible: Boolean) -> Unit, searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), ) { - val tvLazyColumnState = rememberTvLazyListState() + val lazyColumnState = rememberLazyListState() val shouldShowTopBar by remember { derivedStateOf { - tvLazyColumnState.firstVisibleItemIndex == 0 && - tvLazyColumnState.firstVisibleItemScrollOffset < 100 + lazyColumnState.firstVisibleItemIndex == 0 && + lazyColumnState.firstVisibleItemScrollOffset < 100 } } @@ -101,7 +101,6 @@ fun SearchScreen( ) } } - } @OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class) @@ -111,7 +110,7 @@ fun SearchResult( searchMovies: (queryString: String) -> Unit, onMovieClick: (movie: Movie) -> Unit, modifier: Modifier = Modifier, - tvLazyColumnState: TvLazyListState = rememberTvLazyListState(), + lazyColumnState: LazyListState = rememberLazyListState(), ) { val childPadding = rememberChildPadding() var searchQuery by remember { mutableStateOf("") } @@ -120,9 +119,9 @@ fun SearchResult( val tfInteractionSource = remember { MutableInteractionSource() } val isTfFocused by tfInteractionSource.collectIsFocusedAsState() - TvLazyColumn( + LazyColumn( modifier = modifier, - state = tvLazyColumnState + state = lazyColumnState ) { item { Surface( @@ -141,7 +140,8 @@ fun SearchResult( width = if (isTfFocused) 2.dp else 1.dp, color = animateColorAsState( targetValue = if (isTfFocused) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.border, label = "" + else MaterialTheme.colorScheme.border, + label = "" ).value ), shape = JetStreamCardShape @@ -204,7 +204,7 @@ fun SearchResult( ) ), keyboardOptions = KeyboardOptions( - autoCorrect = false, + autoCorrectEnabled = false, imeAction = ImeAction.Search ), keyboardActions = KeyboardActions( @@ -226,7 +226,7 @@ fun SearchResult( modifier = Modifier .fillMaxSize() .padding(top = childPadding.top * 2), - movies = movieList + movieList = movieList ) { selectedMovie -> onMovieClick(selectedMovie) } } } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/search/SearchScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/search/SearchScreenViewModel.kt index c3c15d93..1be09068 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/search/SearchScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/search/SearchScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -21,11 +21,11 @@ import androidx.lifecycle.viewModelScope import com.google.jetstream.data.entities.MovieList import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class SearchScreenViewModel @Inject constructor( @@ -49,10 +49,9 @@ class SearchScreenViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000), initialValue = SearchState.Done(MovieList()) ) - } sealed interface SearchState { data object Searching : SearchState data class Done(val movieList: MovieList) : SearchState -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/shows/ShowScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/shows/ShowScreenViewModel.kt index 37da36d7..5b48da70 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/shows/ShowScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/shows/ShowScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -21,10 +21,10 @@ import androidx.lifecycle.viewModelScope import com.google.jetstream.data.entities.MovieList import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class ShowScreenViewModel @Inject constructor( @@ -41,11 +41,12 @@ class ShowScreenViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000), initialValue = ShowScreenUiState.Loading ) - } sealed interface ShowScreenUiState { - object Loading : ShowScreenUiState - data class Ready(val bingeWatchDramaList: MovieList, val tvShowList: MovieList): ShowScreenUiState - -} \ No newline at end of file + data object Loading : ShowScreenUiState + data class Ready( + val bingeWatchDramaList: MovieList, + val tvShowList: MovieList + ) : ShowScreenUiState +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/shows/ShowsScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/shows/ShowsScreen.kt index b3e10291..ba7be38f 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/shows/ShowsScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/shows/ShowsScreen.kt @@ -19,6 +19,8 @@ package com.google.jetstream.presentation.screens.shows import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -28,13 +30,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.rememberTvLazyListState -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.Text import com.google.jetstream.data.entities.Movie import com.google.jetstream.data.entities.MovieList import com.google.jetstream.data.util.StringConstants +import com.google.jetstream.presentation.common.Loading import com.google.jetstream.presentation.common.MoviesRow import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding import com.google.jetstream.presentation.screens.movies.MoviesScreenMovieList @@ -49,7 +48,7 @@ fun ShowsScreen( val uiState = showScreenViewModel.uiState.collectAsStateWithLifecycle() when (val currentState = uiState.value) { is ShowScreenUiState.Loading -> { - Loading() + Loading(modifier = Modifier.fillMaxSize()) } is ShowScreenUiState.Ready -> { @@ -75,11 +74,11 @@ private fun Catalog( modifier: Modifier = Modifier ) { val childPadding = rememberChildPadding() - val tvLazyListState = rememberTvLazyListState() + val lazyListState = rememberLazyListState() val shouldShowTopBar by remember { derivedStateOf { - tvLazyListState.firstVisibleItemIndex == 0 && - tvLazyListState.firstVisibleItemScrollOffset == 0 + lazyListState.firstVisibleItemIndex == 0 && + lazyListState.firstVisibleItemScrollOffset == 0 } } @@ -87,12 +86,12 @@ private fun Catalog( onScroll(shouldShowTopBar) } LaunchedEffect(isTopBarVisible) { - if (isTopBarVisible) tvLazyListState.animateScrollToItem(0) + if (isTopBarVisible) lazyListState.animateScrollToItem(0) } - TvLazyColumn( + LazyColumn( modifier = modifier, - state = tvLazyListState, + state = lazyListState, contentPadding = PaddingValues(top = childPadding.top, bottom = 104.dp) ) { item { @@ -105,15 +104,9 @@ private fun Catalog( MoviesRow( modifier = Modifier.padding(top = childPadding.top), title = StringConstants.Composable.BingeWatchDramasTitle, - movies = bingeWatchDramaList, - onMovieClick = onTVShowClick + movieList = bingeWatchDramaList, + onMovieSelected = onTVShowClick ) } } } - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -private fun Loading(modifier: Modifier = Modifier) { - Text(text = "Loading...", modifier = modifier) -} \ No newline at end of file diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt index bf903bd9..ebb07290 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt @@ -22,6 +22,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AutoAwesomeMotion @@ -52,6 +53,8 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.ui.PlayerView import com.google.jetstream.data.entities.MovieDetails import com.google.jetstream.data.util.StringConstants +import com.google.jetstream.presentation.common.Error +import com.google.jetstream.presentation.common.Loading import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerControlsIcon import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerMainFrame import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerMediaTitle @@ -66,8 +69,8 @@ import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPla import com.google.jetstream.presentation.screens.videoPlayer.components.rememberVideoPlayerPulseState import com.google.jetstream.presentation.screens.videoPlayer.components.rememberVideoPlayerState import com.google.jetstream.presentation.utils.handleDPadKeyEvents -import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.delay object VideoPlayerScreen { const val MovieIdBundleKey = "movieId" @@ -88,8 +91,12 @@ fun VideoPlayerScreen( // TODO: Handle Loading & Error states when (val s = uiState) { - is VideoPlayerScreenUiState.Loading -> {} - is VideoPlayerScreenUiState.Error -> {} + is VideoPlayerScreenUiState.Loading -> { + Loading(modifier = Modifier.fillMaxSize()) + } + is VideoPlayerScreenUiState.Error -> { + Error(modifier = Modifier.fillMaxSize()) + } is VideoPlayerScreenUiState.Done -> { VideoPlayerScreenContent( movieDetails = s.movieDetails, @@ -116,7 +123,8 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un emptyList() } else { listOf( - MediaItem.SubtitleConfiguration.Builder(Uri.parse(movieDetails.subtitleUri)) + MediaItem.SubtitleConfiguration + .Builder(Uri.parse(movieDetails.subtitleUri)) .setMimeType("application/vtt") .setLanguage("en") .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) @@ -199,7 +207,6 @@ fun VideoPlayerControls( } } - VideoPlayerMainFrame( mediaTitle = { VideoPlayerMediaTitle( @@ -257,7 +264,6 @@ fun VideoPlayerControls( ) } - @androidx.annotation.OptIn(UnstableApi::class) @Composable private fun rememberExoPlayer(context: Context) = remember { @@ -280,22 +286,22 @@ private fun Modifier.dPadEvents( videoPlayerState: VideoPlayerState, pulseState: VideoPlayerPulseState ): Modifier = this.handleDPadKeyEvents( - onLeft = { - if (!videoPlayerState.controlsVisible) { - exoPlayer.seekBack() - pulseState.setType(BACK) - } - }, - onRight = { - if (!videoPlayerState.controlsVisible) { - exoPlayer.seekForward() - pulseState.setType(FORWARD) - } - }, - onUp = { videoPlayerState.showControls() }, - onDown = { videoPlayerState.showControls() }, - onEnter = { - exoPlayer.pause() - videoPlayerState.showControls() + onLeft = { + if (!videoPlayerState.controlsVisible) { + exoPlayer.seekBack() + pulseState.setType(BACK) + } + }, + onRight = { + if (!videoPlayerState.controlsVisible) { + exoPlayer.seekForward() + pulseState.setType(FORWARD) } -) \ No newline at end of file + }, + onUp = { videoPlayerState.showControls() }, + onDown = { videoPlayerState.showControls() }, + onEnter = { + exoPlayer.pause() + videoPlayerState.showControls() + } +) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreenViewModel.kt index 86bd0c49..5a79da15 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreenViewModel.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -23,10 +23,10 @@ import androidx.lifecycle.viewModelScope import com.google.jetstream.data.entities.MovieDetails import com.google.jetstream.data.repositories.MovieRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class VideoPlayerScreenViewModel @Inject constructor( @@ -54,4 +54,4 @@ sealed class VideoPlayerScreenUiState { object Loading : VideoPlayerScreenUiState() object Error : VideoPlayerScreenUiState() data class Done(val movieDetails: MovieDetails) : VideoPlayerScreenUiState() -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControllerText.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControllerText.kt index eefa05c4..23eea620 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControllerText.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControllerText.kt @@ -22,8 +22,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.Text import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text @OptIn(ExperimentalTvMaterial3Api::class) @Composable diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMainFrame.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMainFrame.kt index f2d60611..48e4d07a 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMainFrame.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMainFrame.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2024 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.videoPlayer.components import androidx.compose.foundation.background @@ -44,7 +60,6 @@ fun VideoPlayerMainFrame( } } - @Preview(device = "id:tv_4k") @Composable private fun MediaPlayerMainFramePreviewLayout() { @@ -118,4 +133,4 @@ private fun MediaPlayerMainFramePreviewLayoutWithoutMore() { }, more = null, ) -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMediaTitle.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMediaTitle.kt index 9de5c8a8..3c1b0875 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMediaTitle.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMediaTitle.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2024 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.videoPlayer.components import androidx.compose.foundation.background @@ -130,4 +146,4 @@ private fun VideoPlayerMediaTitlePreviewAd() { ) } } -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerOverlay.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerOverlay.kt index 2c2e4c2d..e4b164bd 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerOverlay.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerOverlay.kt @@ -78,8 +78,10 @@ fun VideoPlayerOverlay( } Column { - Box(Modifier.weight(1f), - contentAlignment = Alignment.BottomCenter) { + Box( + Modifier.weight(1f), + contentAlignment = Alignment.BottomCenter + ) { subtitles() } @@ -150,4 +152,4 @@ private fun VideoPlayerOverlayPreview() { ) } } -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerPulse.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerPulse.kt index 55f56304..0141ea7b 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerPulse.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerPulse.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2024 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.videoPlayer.components import androidx.compose.foundation.background @@ -19,10 +35,11 @@ import androidx.compose.ui.unit.dp import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulse.Type.NONE +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.debounce -import kotlin.time.Duration.Companion.seconds object VideoPlayerPulse { enum class Type { FORWARD, BACK, NONE } @@ -57,6 +74,7 @@ class VideoPlayerPulseState { private val channel = Channel(Channel.CONFLATED) + @OptIn(FlowPreview::class) suspend fun observe() { channel.consumeAsFlow() .debounce(2.seconds) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt index d8afb4b5..976b05fb 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2024 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.videoPlayer.components import androidx.compose.foundation.layout.Row @@ -24,21 +40,19 @@ fun VideoPlayerSeeker( ) { val contentProgressString = contentProgress.toComponents { h, m, s, _ -> - if(h > 0) { + if (h > 0) { "$h:${m.padStartWith0()}:${s.padStartWith0()}" } else { "${m.padStartWith0()}:${s.padStartWith0()}" } - } val contentDurationString = contentDuration.toComponents { h, m, s, _ -> - if(h > 0) { + if (h > 0) { "$h:${m.padStartWith0()}:${s.padStartWith0()}" } else { "${m.padStartWith0()}:${s.padStartWith0()}" } - } Row( diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt index 51b67aa6..1dcd80f6 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.flow.consumeAsFlow @@ -42,6 +43,7 @@ class VideoPlayerState internal constructor( private val channel = Channel(CONFLATED) + @OptIn(FlowPreview::class) suspend fun observe() { channel.consumeAsFlow() .debounce { it.toLong() * 1000 } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/theme/JetStreamFocusTheme.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/theme/JetStreamFocusTheme.kt index 81619be9..d71383a4 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/theme/JetStreamFocusTheme.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/theme/JetStreamFocusTheme.kt @@ -30,4 +30,4 @@ val JetStreamBorderWidth = 3.dp /** * Space to be given below every Lazy (or scrollable) vertical list throughout the app */ -val JetStreamBottomListPadding = 28.dp \ No newline at end of file +val JetStreamBottomListPadding = 28.dp diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/BringIntoViewIfChildrenAreFocused.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/BringIntoViewIfChildrenAreFocused.kt index 91523f5b..6f1eb9ba 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/BringIntoViewIfChildrenAreFocused.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/BringIntoViewIfChildrenAreFocused.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -17,39 +17,51 @@ package com.google.jetstream.presentation.utils import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.relocation.BringIntoViewResponder import androidx.compose.foundation.relocation.bringIntoViewResponder -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.toSize @Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta @OptIn(ExperimentalFoundationApi::class) -internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed( +// ToDo: Migrate to Modifier.Node and stop using composed function. +internal fun Modifier.bringIntoViewIfChildrenAreFocused( + paddingValues: PaddingValues = PaddingValues() +): Modifier = composed( inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" }, factory = { + val pxOffset = with(LocalDensity.current) { + val y = (paddingValues.calculateBottomPadding() - paddingValues.calculateTopPadding()) + .toPx() + Offset.Zero.copy(y = y) + } var myRect: Rect = Rect.Zero + val responder = object : BringIntoViewResponder { + // return the current rectangle and ignoring the child rectangle received. + @ExperimentalFoundationApi + override fun calculateRectForParent(localRect: Rect): Rect { + return myRect + } + + // The container is not expected to be scrollable. Hence the child is + // already in view with respect to the container. + @ExperimentalFoundationApi + override suspend fun bringChildIntoView(localRect: () -> Rect?) { + } + } + this .onSizeChanged { - myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat())) + val size = it.toSize() + myRect = Rect(pxOffset, size) } - .bringIntoViewResponder( - remember { - object : BringIntoViewResponder { - // return the current rectangle and ignoring the child rectangle received. - @ExperimentalFoundationApi - override fun calculateRectForParent(localRect: Rect): Rect = myRect - - // The container is not expected to be scrollable. Hence the child is - // already in view with respect to the container. - @ExperimentalFoundationApi - override suspend fun bringChildIntoView(localRect: () -> Rect?) {} - } - } - ) + .bringIntoViewResponder(responder) } -) \ No newline at end of file +) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/Colors.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/Colors.kt index c544db89..370cea64 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/Colors.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/Colors.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/GradientBg.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/GradientBg.kt index b5efb447..eaa0a5bf 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/GradientBg.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/GradientBg.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.unit.dp - val pairs = listOf( Coral to LightYellow, Red300 to BlueGray300, @@ -48,4 +47,4 @@ fun GradientBg() { .fillMaxWidth() .height(200.dp) ) -} \ No newline at end of file +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/ModifierUtils.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/ModifierUtils.kt index 38bb53bc..8d89c66c 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/ModifierUtils.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/ModifierUtils.kt @@ -31,20 +31,6 @@ import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.layout.onPlaced -private val DPadEventsKeyCodes = listOf( - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT, - KeyEvent.KEYCODE_DPAD_UP, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, - KeyEvent.KEYCODE_DPAD_DOWN, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_NUMPAD_ENTER -) - /** * Handles horizontal (Left & Right) D-Pad Keys and consumes the event(s) so that the focus doesn't * accidentally move to another element. @@ -58,28 +44,28 @@ fun Modifier.handleDPadKeyEvents( if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) block() } - if (DPadEventsKeyCodes.contains(it.nativeKeyEvent.keyCode)) { - when (it.nativeKeyEvent.keyCode) { - KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> { - onLeft?.apply { - onActionUp(::invoke) - return@onPreviewKeyEvent true - } + when (it.nativeKeyEvent.keyCode) { + KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> { + onLeft?.apply { + onActionUp(::invoke) + return@onPreviewKeyEvent true } - KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> { - onRight?.apply { - onActionUp(::invoke) - return@onPreviewKeyEvent true - } + } + + KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> { + onRight?.apply { + onActionUp(::invoke) + return@onPreviewKeyEvent true } - KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> { - onEnter?.apply { - onActionUp(::invoke) - return@onPreviewKeyEvent true - } + } + + KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> { + onEnter?.invoke().also { + return@onPreviewKeyEvent true } } } + false } @@ -94,20 +80,24 @@ fun Modifier.handleDPadKeyEvents( onEnter: (() -> Unit)? = null ) = onKeyEvent { - if (DPadEventsKeyCodes.contains(it.nativeKeyEvent.keyCode) && it.nativeKeyEvent.action == KeyEvent.ACTION_UP) { + if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) { when (it.nativeKeyEvent.keyCode) { KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> { onLeft?.invoke().also { return@onKeyEvent true } } + KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> { onRight?.invoke().also { return@onKeyEvent true } } + KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP -> { onUp?.invoke().also { return@onKeyEvent true } } + KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN -> { onDown?.invoke().also { return@onKeyEvent true } } + KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> { onEnter?.invoke().also { return@onKeyEvent true } } @@ -207,4 +197,4 @@ fun Modifier.ifElse( condition: Boolean, ifTrueModifier: Modifier, ifFalseModifier: Modifier = Modifier -): Modifier = ifElse({ condition }, ifTrueModifier, ifFalseModifier) \ No newline at end of file +): Modifier = ifElse({ condition }, ifTrueModifier, ifFalseModifier) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/Padding.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/Padding.kt index 7b835ffa..a6ab20d1 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/Padding.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/utils/Padding.kt @@ -25,4 +25,4 @@ data class Padding( val top: Dp, val end: Dp, val bottom: Dp, -) \ No newline at end of file +) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/tvmaterial/Dialog.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/tvmaterial/Dialog.kt index dfd151ea..6c609df7 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/tvmaterial/Dialog.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/tvmaterial/Dialog.kt @@ -20,8 +20,8 @@ import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.rememberTransition import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement @@ -385,13 +385,13 @@ fun Dialog( var alphaTransitionState by remember { mutableStateOf(MutableTransitionState(AnimationStage.Intro)) } - val alphaTransition = updateTransition(alphaTransitionState, label = "alphaTransition") + val alphaTransition = rememberTransition(alphaTransitionState, label = "alphaTransition") // Transitions for dialog content scaling. var scaleTransitionState by remember { mutableStateOf(MutableTransitionState(AnimationStage.Intro)) } - val scaleTransition = updateTransition(scaleTransitionState, label = "scaleTransition") + val scaleTransition = rememberTransition(scaleTransitionState, label = "scaleTransition") if (showDialog || alphaTransitionState.targetState != AnimationStage.Intro || scaleTransitionState.targetState != AnimationStage.Intro @@ -484,7 +484,7 @@ internal fun DialogFlowRow( // Return whether the placeable can be added to the current sequence. fun canAddToCurrentSequence(placeable: Placeable) = currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() + - placeable.width <= constraints.maxWidth + placeable.width <= constraints.maxWidth // Store current sequence information and start a new sequence. fun startNewSequence() { @@ -531,7 +531,7 @@ internal fun DialogFlowRow( sequences.forEachIndexed { i, placeables -> val childrenMainAxisSizes = IntArray(placeables.size) { j -> placeables[j].width + - if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 + if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 } val arrangement = Arrangement.Bottom // Handle vertical direction diff --git a/JetStreamCompose/jetstream/src/main/res/values/strings.xml b/JetStreamCompose/jetstream/src/main/res/values/strings.xml index 56849726..0f737cb4 100644 --- a/JetStreamCompose/jetstream/src/main/res/values/strings.xml +++ b/JetStreamCompose/jetstream/src/main/res/values/strings.xml @@ -37,7 +37,6 @@ https://www.apache.org/licenses/LICENSE-2.0 Top 10 in the US %s icon %s language option - Loading movie category %s Movies TV Shows @@ -46,4 +45,6 @@ https://www.apache.org/licenses/LICENSE-2.0 Unknown filter Ad Live + Loading… + Wops, something went wrong… \ No newline at end of file diff --git a/JetStreamCompose/spotless/copyright.kt b/JetStreamCompose/spotless/copyright.kt new file mode 100644 index 00000000..379c54f9 --- /dev/null +++ b/JetStreamCompose/spotless/copyright.kt @@ -0,0 +1,16 @@ +/* + * Copyright $YEAR 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. + */ +