From 8199adca7f9bc91db63942112f1f9a52dbdb4da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 16 Sep 2024 15:16:18 +0200 Subject: [PATCH] Update to AndroidX Navigation 2.8.0 (#701) --- gradle/libs.versions.toml | 2 +- pillarbox-demo-shared/build.gradle.kts | 4 +- .../demo/shared/ui/HomeDestination.kt | 18 +- .../demo/shared/ui/NavigationRoutes.kt | 91 ++++-- .../shared/ui/integrationLayer/ContentList.kt | 267 +++++------------- .../data/ContentListSections.kt | 19 -- .../ui/player/settings/SettingsRoutes.kt | 28 +- pillarbox-demo-tv/build.gradle.kts | 1 + .../srgssr/pillarbox/demo/tv/MainActivity.kt | 4 +- .../pillarbox/demo/tv/MainNavigation.kt | 7 +- .../demo/tv/ui/components/TVDemoTopBar.kt | 3 +- .../demo/tv/ui/examples/ExamplesHome.kt | 18 +- .../pillarbox/demo/tv/ui/lists/ListsHome.kt | 192 +++++++------ .../settings/PlaybackSettingsDrawer.kt | 16 +- pillarbox-demo/build.gradle.kts | 1 + .../srgssr/pillarbox/demo/MainNavigation.kt | 62 ++-- .../pillarbox/demo/ui/lists/ListsHome.kt | 80 ++++-- .../settings/PlaybackSettingsContent.kt | 22 +- .../demo/ui/showcases/ShowcasesHome.kt | 44 +-- .../demo/ui/showcases/ShowcasesNavigation.kt | 26 +- 20 files changed, 419 insertions(+), 486 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5207bb65d..2fb4547d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ androidx-datastore = "1.1.1" androidx-fragment = "1.8.3" androidx-lifecycle = "2.8.5" androidx-media3 = "1.4.1" -androidx-navigation = "2.7.7" +androidx-navigation = "2.8.0" androidx-paging = "3.3.2" androidx-test-core = "1.6.1" androidx-test-ext-junit = "1.2.1" diff --git a/pillarbox-demo-shared/build.gradle.kts b/pillarbox-demo-shared/build.gradle.kts index c70f8ae58..4cfcef222 100644 --- a/pillarbox-demo-shared/build.gradle.kts +++ b/pillarbox-demo-shared/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.pillarbox.android.library) alias(libs.plugins.pillarbox.android.library.compose) + alias(libs.plugins.kotlin.serialization) } dependencies { @@ -31,10 +32,11 @@ dependencies { api(libs.androidx.lifecycle.viewmodel) api(libs.androidx.media3.common) implementation(libs.androidx.media3.exoplayer) - api(libs.androidx.navigation.common) + implementation(libs.androidx.navigation.common) api(libs.androidx.navigation.runtime) implementation(libs.androidx.paging.common) api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.serialization.core) implementation(libs.okhttp) api(libs.srg.data) api(libs.srg.dataprovider.paging) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/HomeDestination.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/HomeDestination.kt index dca4edc06..f44bd3389 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/HomeDestination.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/HomeDestination.kt @@ -24,43 +24,43 @@ import ch.srgssr.pillarbox.demo.shared.R * @property imageVector image vector */ sealed class HomeDestination( - val route: String, + val route: NavigationRoutes, @StringRes val labelResId: Int, val imageVector: ImageVector ) { /** * Examples home page containing all kinds of streams */ - data object Examples : HomeDestination(NavigationRoutes.homeSamples, R.string.examples, Icons.Default.Home) + data object Examples : HomeDestination(NavigationRoutes.HomeSamples, R.string.examples, Icons.Default.Home) /** * Streams home page */ - data object ShowCases : HomeDestination(NavigationRoutes.homeShowcases, R.string.showcases, Icons.Default.Movie) + data object ShowCases : HomeDestination(NavigationRoutes.HomeShowcases, R.string.showcases, Icons.Default.Movie) /** * Integration layer list home page */ - data object Lists : HomeDestination(NavigationRoutes.homeLists, R.string.lists, Icons.AutoMirrored.Filled.ViewList) + data object Lists : HomeDestination(NavigationRoutes.HomeLists, R.string.lists, Icons.AutoMirrored.Filled.ViewList) /** * Info home page */ - data object Search : HomeDestination(NavigationRoutes.searchHome, R.string.search, Icons.Default.Search) + data object Search : HomeDestination(NavigationRoutes.SearchHome, R.string.search, Icons.Default.Search) /** * Settings home page */ - data object Settings : HomeDestination(NavigationRoutes.settingsHome, R.string.settings, Icons.Default.Settings) + data object Settings : HomeDestination(NavigationRoutes.SettingsHome, R.string.settings, Icons.Default.Settings) } /** * Navigate as a top level destination. * - * @param destination The [HomeDestination] to navigate to. + * @param route The [NavigationRoutes] to navigate to. */ -fun NavController.navigate(destination: HomeDestination) { - navigate(destination.route) { +fun NavController.navigateTopLevel(route: NavigationRoutes) { + navigate(route) { // Pop up to the start destination of the graph to // avoid building up a large stack of destinations // on the back stack as users select items diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt index e3455a681..92849511e 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt @@ -4,31 +4,74 @@ */ package ch.srgssr.pillarbox.demo.shared.ui +import kotlinx.serialization.Serializable + /** * Navigation stores all routes available */ -@Suppress("UndocumentedPublicProperty") -object NavigationRoutes { - const val homeSamples = "home_samples" - const val homeSample = "home_sample" - const val homeShowcases = "home_showcases" - const val showcaseList = "showcase_list" - const val showcasePlaybackSettings = "showcase_playback_settings" - const val story = "story" - const val simplePlayer = "simple_player" - const val adaptive = "adaptive" - const val playerSwap = "player_swap" - const val exoPlayerSample = "exoplayer_sample" - const val trackingSample = "tracking_sample" - const val updatableSample = "updatable_sample" - const val smoothSeeking = "smoothSeeking_sample" - const val startAtGivenTime = "start_given_time_sample" - const val video360 = "video_360" - const val chapters = "chapters" - - const val homeLists = "home_lists" - const val contentLists = "content_lists" - const val contentList = "content_list" - const val searchHome = "search_home" - const val settingsHome = "settings_home" +@Serializable +@Suppress("UndocumentedPublicClass", "UndocumentedPublicProperty") +sealed interface NavigationRoutes { + @Serializable + data object HomeSamples : NavigationRoutes + + @Serializable + data class HomeSample(val index: Int) : NavigationRoutes + + @Serializable + data object HomeShowcases : NavigationRoutes + + @Serializable + data object ShowcaseList : NavigationRoutes + + @Serializable + data object ShowcasePlaybackSettings : NavigationRoutes + + @Serializable + data object Story : NavigationRoutes + + @Serializable + data object SimplePlayer : NavigationRoutes + + @Serializable + data object Adaptive : NavigationRoutes + + @Serializable + data object PlayerSwap : NavigationRoutes + + @Serializable + data object ExoPlayerSample : NavigationRoutes + + @Serializable + data object TrackingSample : NavigationRoutes + + @Serializable + data object UpdatableSample : NavigationRoutes + + @Serializable + data object SmoothSeeking : NavigationRoutes + + @Serializable + data object StartAtGivenTime : NavigationRoutes + + @Serializable + data object Video360 : NavigationRoutes + + @Serializable + data object Chapters : NavigationRoutes + + @Serializable + data object HomeLists : NavigationRoutes + + @Serializable + data object ContentLists : NavigationRoutes + + @Serializable + data class ContentList(val index: Int) : NavigationRoutes + + @Serializable + data object SearchHome : NavigationRoutes + + @Serializable + data object SettingsHome : NavigationRoutes } diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/ContentList.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/ContentList.kt index 0f0744de0..22696fa14 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/ContentList.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/ContentList.kt @@ -4,250 +4,139 @@ */ package ch.srgssr.pillarbox.demo.shared.ui.integrationLayer -import androidx.navigation.NavBackStackEntry import ch.srg.dataProvider.integrationlayer.request.parameters.Bu - -private const val RootRoute = "content" +import kotlinx.serialization.Serializable /** * Content list that handle destination route */ -@Suppress("UndocumentedPublicFunction", "UndocumentedPublicProperty", "UndocumentedPublicClass") +@Serializable +@Suppress("UndocumentedPublicProperty", "UndocumentedPublicClass") sealed interface ContentList { - val destinationRoute: String - val destinationTitle: String + // Type-safe navigation does not yet support property-level @Serializable + // So, we use the BU name as a property, and recreate the BU on demand + // See: https://issuetracker.google.com/issues/348468840 sealed interface ContentListWithBu : ContentList { + val buName: String + val bu: Bu + get() = Bu(buName) override val destinationTitle: String get() = bu.name.uppercase() } - interface ContentListFactory { - val route: String - val trackerTitle: String - - fun parse(backStackEntry: NavBackStackEntry): T - } - - data class TVTopics(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/tv/topics" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/tv/topics" - override val trackerTitle = "tv-topics" - - override fun parse(backStackEntry: NavBackStackEntry): TVTopics { - return TVTopics(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class TVTopics private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } + @Serializable data class LatestMediaForTopic( val urn: String, val topic: String ) : ContentList { - override val destinationRoute = "$RootRoute/latestMediaByTopic/$urn?topic=$topic" - override val destinationTitle = topic - - companion object : ContentListFactory { - override val route = "$RootRoute/latestMediaByTopic/{topicUrn}?topic={topic}" - override val trackerTitle = "latest-media-for-topic" - - override fun parse(backStackEntry: NavBackStackEntry): LatestMediaForTopic { - val arguments = backStackEntry.arguments - - return LatestMediaForTopic( - urn = arguments?.getString("topicUrn").orEmpty(), - topic = arguments?.getString("topic").orEmpty() - ) - } - } } + @Serializable data class LatestMediaForShow( val urn: String, - val show: String, + val show: String ) : ContentList { - override val destinationRoute = "$RootRoute/latestMediaByShow/$urn?show=$show" - override val destinationTitle = show - - companion object : ContentListFactory { - override val route = "$RootRoute/latestMediaByShow/{showUrn}?show={show}" - override val trackerTitle = "latest-media-for-show" - - override fun parse(backStackEntry: NavBackStackEntry): LatestMediaForShow { - val arguments = backStackEntry.arguments - - return LatestMediaForShow( - urn = arguments?.getString("showUrn").orEmpty(), - show = arguments?.getString("show").orEmpty(), - ) - } - } } - data class TVShows(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/tv/shows" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/tv/shows" - override val trackerTitle = "tv-shows" - - override fun parse(backStackEntry: NavBackStackEntry): TVShows { - return TVShows(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class TVShows private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } - data class TVLatestMedias(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/tv/latestMedia" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/tv/latestMedia" - override val trackerTitle = "tv-latest-videos" - - override fun parse(backStackEntry: NavBackStackEntry): TVLatestMedias { - return TVLatestMedias(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class TVLatestMedias private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } - data class TVLivestreams(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/tv/livestream" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/tv/livestream" - override val trackerTitle = "tv-livestreams" - - override fun parse(backStackEntry: NavBackStackEntry): TVLivestreams { - return TVLivestreams(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class TVLivestreams private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } - data class TVLiveCenter(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/tv/livecenter" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/tv/livecenter" - override val trackerTitle = "live-center" - - override fun parse(backStackEntry: NavBackStackEntry): TVLiveCenter { - return TVLiveCenter(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class TVLiveCenter private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } - data class TVLiveWeb(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/tv/liveweb" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/tv/liveweb" - override val trackerTitle = "live-web" - - override fun parse(backStackEntry: NavBackStackEntry): TVLiveWeb { - return TVLiveWeb(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class TVLiveWeb private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } - data class RadioLiveStreams(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/radio/livestream" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/radio/livestream" - override val trackerTitle = "radio-livestreams" - - override fun parse(backStackEntry: NavBackStackEntry): RadioLiveStreams { - return RadioLiveStreams(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class RadioLiveStreams private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } - data class RadioShows(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/radio/shows" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/radio/shows" - override val trackerTitle = "shows" - - override fun parse(backStackEntry: NavBackStackEntry): RadioShows { - return RadioShows(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class RadioShows private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } - data class RadioShowsForChannel( - override val bu: Bu, + @Serializable + @ConsistentCopyVisibility + data class RadioShowsForChannel private constructor( + override val buName: String, val channelId: String, - private val channelTitle: String + private val channelTitle: String, ) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/radio/shows/$channelId/$channelTitle" override val destinationTitle = channelTitle - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/radio/shows/{channelId}/{channelTitle}" - override val trackerTitle = "shows-for-channel" - - override fun parse(backStackEntry: NavBackStackEntry): RadioShowsForChannel { - return RadioShowsForChannel( - bu = backStackEntry.readBu(), - channelId = backStackEntry.readChannelId(), - channelTitle = backStackEntry.readChannelTitle() - ) - } - } + constructor(bu: Bu, channelId: String, channelTitle: String) : this(bu.name, channelId, channelTitle) } - data class RadioLatestMedias(override val bu: Bu) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/radio/latestMedia" - - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/radio/latestMedia" - override val trackerTitle = "latest-audios" - - override fun parse(backStackEntry: NavBackStackEntry): RadioLatestMedias { - return RadioLatestMedias(backStackEntry.readBu()) - } - } + @Serializable + @ConsistentCopyVisibility + data class RadioLatestMedias private constructor( + override val buName: String, + ) : ContentListWithBu { + constructor(bu: Bu) : this(bu.name) } - data class RadioLatestMediasForChannel( - override val bu: Bu, + @Serializable + @ConsistentCopyVisibility + data class RadioLatestMediasForChannel private constructor( + override val buName: String, val channelId: String, - private val channelTitle: String + private val channelTitle: String, ) : ContentListWithBu { - override val destinationRoute = "$RootRoute/$bu/radio/latestMedia/$channelId/$channelTitle" override val destinationTitle = channelTitle - companion object : ContentListFactory { - override val route = "$RootRoute/{bu}/radio/latestMedia/{channelId}/{channelTitle}" - override val trackerTitle = "latest-audios-for-channel" - - override fun parse(backStackEntry: NavBackStackEntry): RadioLatestMediasForChannel { - return RadioLatestMediasForChannel( - bu = backStackEntry.readBu(), - channelId = backStackEntry.readChannelId(), - channelTitle = backStackEntry.readChannelTitle() - ) - } - } + constructor(bu: Bu, channelId: String, channelTitle: String) : this(bu.name, channelId, channelTitle) } } - -private fun NavBackStackEntry.readBu(): Bu { - return arguments?.getString("bu")?.let { Bu(it) } ?: Bu.RTS -} - -private fun NavBackStackEntry.readChannelId(): String { - return arguments?.getString("channelId").orEmpty() -} - -private fun NavBackStackEntry.readChannelTitle(): String { - return arguments?.getString("channelTitle").orEmpty() -} diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSections.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSections.kt index 1ceb534a1..b56393164 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSections.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSections.kt @@ -27,22 +27,3 @@ val contentListSections = listOf( ContentListSection("Radio Latest Audios", busWithoutSWI.map { ContentList.RadioLatestMedias(it) }), ContentListSection("Radio Shows", busWithoutSWI.map { ContentList.RadioShows(it) }), ) - -/** - * All the types of content list in the "Lists" tab. - */ -val contentListFactories = listOf( - ContentList.TVTopics, - ContentList.TVShows, - ContentList.TVLatestMedias, - ContentList.TVLivestreams, - ContentList.TVLiveCenter, - ContentList.TVLiveWeb, - ContentList.RadioLiveStreams, - ContentList.RadioLatestMedias, - ContentList.RadioShows, - ContentList.LatestMediaForShow, - ContentList.LatestMediaForTopic, - ContentList.RadioShowsForChannel, - ContentList.RadioLatestMediasForChannel, -) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/SettingsRoutes.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/SettingsRoutes.kt index 81dda654c..283f25ee2 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/SettingsRoutes.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/SettingsRoutes.kt @@ -4,48 +4,56 @@ */ package ch.srgssr.pillarbox.demo.shared.ui.player.settings +import kotlinx.serialization.Serializable + /** * All the routes used in the player's settings. - * - * @property route The route of the setting. */ -sealed class SettingsRoutes(val route: String) { +@Serializable +sealed interface SettingsRoutes { /** * The route for the main screen of the settings. */ - data object Main : SettingsRoutes(route = "settings") + @Serializable + data object Main : SettingsRoutes /** * The route for the playback speed setting. */ - data object PlaybackSpeed : SettingsRoutes(route = "settings/playback_speed") + @Serializable + data object PlaybackSpeed : SettingsRoutes /** * The route for the subtitles setting. */ - data object Subtitles : SettingsRoutes(route = "settings/subtitles") + @Serializable + data object Subtitles : SettingsRoutes /** * The route for the audio track setting. */ - data object AudioTrack : SettingsRoutes(route = "settings/audio_track") + @Serializable + data object AudioTrack : SettingsRoutes /** * The route for the video track setting. */ - data object VideoTrack : SettingsRoutes(route = "settings/video_track") + @Serializable + data object VideoTrack : SettingsRoutes /** * The route for the metrics overlay setting. * * @property enabled Whether the metrics overlay is enabled. */ + @Serializable data class MetricsOverlay( val enabled: Boolean, - ) : SettingsRoutes(route = "settings/metrics_overlay") + ) : SettingsRoutes /** * The route for the "Stats for nerds" screen. */ - data object StatsForNerds : SettingsRoutes(route = "settings/stats_for_nerds") + @Serializable + data object StatsForNerds : SettingsRoutes } diff --git a/pillarbox-demo-tv/build.gradle.kts b/pillarbox-demo-tv/build.gradle.kts index 9405cfe2d..c9a27220e 100644 --- a/pillarbox-demo-tv/build.gradle.kts +++ b/pillarbox-demo-tv/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(libs.coil.base) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.core) implementation(libs.srg.data) implementation(libs.srg.dataprovider.retrofit) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainActivity.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainActivity.kt index 1613cb5b7..d0b7ceb70 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainActivity.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainActivity.kt @@ -26,7 +26,7 @@ import androidx.navigation.compose.rememberNavController import androidx.tv.material3.LocalContentColor import androidx.tv.material3.MaterialTheme import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination -import ch.srgssr.pillarbox.demo.shared.ui.navigate +import ch.srgssr.pillarbox.demo.shared.ui.navigateTopLevel import ch.srgssr.pillarbox.demo.tv.ui.components.TVDemoTopBar import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings @@ -67,7 +67,7 @@ class MainActivity : ComponentActivity() { .focusRequester(focusRequester), onDestinationClick = { destination -> selectedDestination = destination - navController.navigate(destination) + navController.navigateTopLevel(destination.route) } ) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainNavigation.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainNavigation.kt index 2b7a2472a..6f26be51d 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainNavigation.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/MainNavigation.kt @@ -18,6 +18,7 @@ import androidx.navigation.compose.rememberNavController import androidx.tv.material3.MaterialTheme import ch.srgssr.pillarbox.demo.shared.di.PlayerModule import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination +import ch.srgssr.pillarbox.demo.shared.ui.NavigationRoutes import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.SearchViewModel import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListSections import ch.srgssr.pillarbox.demo.tv.ui.examples.ExamplesHome @@ -45,7 +46,7 @@ fun MainNavigation( startDestination = startDestination.route, modifier = modifier ) { - composable(HomeDestination.Examples.route) { + composable { val context = LocalContext.current ExamplesHome( @@ -53,13 +54,13 @@ fun MainNavigation( ) } - composable(HomeDestination.Lists.route) { + composable { ListsHome( sections = contentListSections ) } - composable(HomeDestination.Search.route) { + composable { val context = LocalContext.current val ilRepository = remember { PlayerModule.createIlRepository(context) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVDemoTopBar.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVDemoTopBar.kt index b38dd5f7f..9d820e9ce 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVDemoTopBar.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/components/TVDemoTopBar.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Tab @@ -54,7 +55,7 @@ fun TVDemoTopBar( mutableIntStateOf( destinations.indexOfFirst { dest -> - destinationHierarchy.any { it.route == dest.route } + destinationHierarchy.any { it.hasRoute(dest.route::class) } } ) } diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt index dc258f22e..d00ffef16 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt @@ -44,11 +44,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.navigation.toRoute import androidx.tv.material3.Card import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -79,17 +78,17 @@ fun ExamplesHome( NavHost( navController = navController, - startDestination = NavigationRoutes.homeSamples, + startDestination = NavigationRoutes.HomeSamples, modifier = modifier ) { - composable(NavigationRoutes.homeSamples) { + composable { ExamplesSection( columnCount = 4, items = playlists, focusFirstItem = false, navController = navController, onItemClick = { index, _ -> - navController.navigate("${NavigationRoutes.homeSample}/$index") + navController.navigate(NavigationRoutes.HomeSample(index)) } ) { item -> Box( @@ -108,13 +107,8 @@ fun ExamplesHome( } } - composable( - route = "${NavigationRoutes.homeSample}/{index}", - arguments = listOf( - navArgument("index") { type = NavType.IntType } - ) - ) { - val playlistIndex = it.arguments?.getInt("index") ?: 0 + composable { + val playlistIndex = it.toRoute().index val playlist = playlists[playlistIndex] ExamplesSection( diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt index 0ad819017..2e5a04a03 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt @@ -50,12 +50,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.navigation.toRoute import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems @@ -85,7 +85,7 @@ import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.ContentList import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.ContentListViewModel import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.Content import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ContentListSection -import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListFactories +import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListSections import ch.srgssr.pillarbox.demo.tv.R import ch.srgssr.pillarbox.demo.tv.ui.player.PlayerActivity @@ -111,32 +111,76 @@ fun ListsHome( sections: List, modifier: Modifier = Modifier ) { + val context = LocalContext.current + val ilRepository = PlayerModule.createIlRepository(context) val navController = rememberNavController() + val contentClick = { contentList: ContentList, content: Content -> + when (content) { + is Content.Channel -> { + val nextContentList = when (contentList) { + is ContentList.RadioShows -> ContentList.RadioShowsForChannel( + bu = contentList.bu, + channelId = content.id, + channelTitle = content.title + ) + + is ContentList.RadioLatestMedias -> ContentList.RadioLatestMediasForChannel( + bu = contentList.bu, + channelId = content.id, + channelTitle = content.title + ) + + else -> error("Unsupported content list") + } + + navController.navigate(nextContentList) + } + + is Content.Media -> { + val demoItem = DemoItem(title = content.title, uri = content.urn) + + PlayerActivity.startPlayer(context, demoItem) + } + + is Content.Show -> { + val show = ContentList.LatestMediaForShow( + urn = content.urn, + show = content.title, + ) + + navController.navigate(show) + } + + is Content.Topic -> { + val topic = ContentList.LatestMediaForTopic( + urn = content.urn, + topic = content.title + ) + + navController.navigate(topic) + } + } + } NavHost( navController = navController, - startDestination = NavigationRoutes.contentLists, + startDestination = NavigationRoutes.ContentLists, modifier = modifier.fillMaxSize() ) { - composable(NavigationRoutes.contentLists) { + composable { ListsSection( items = sections, focusFirstItem = false, itemToString = { it.title }, navController = navController, onItemClick = { index, _ -> - navController.navigate("${NavigationRoutes.contentList}/$index") + navController.navigate(NavigationRoutes.ContentList(index)) } ) } - composable( - route = "${NavigationRoutes.contentList}/{index}", - arguments = listOf( - navArgument("index") { type = NavType.IntType } - ) - ) { - val sectionIndex = it.arguments?.getInt("index") ?: 0 + composable { + val sectionIndex = it.toRoute().index val section = sections[sectionIndex] ListsSection( @@ -154,88 +198,72 @@ fun ListsHome( ), navController = navController, onItemClick = { _, contentList -> - navController.navigate(contentList.destinationRoute) + navController.navigate(contentList) } ) } - contentListFactories.forEach { contentListFactory -> - composable(route = contentListFactory.route) { - val context = LocalContext.current - val contentList = contentListFactory.parse(it) - val viewModel = viewModel( - factory = ContentListViewModel.Factory( - ilRepository = PlayerModule.createIlRepository(context), - contentList = contentListFactory.parse(it) - ) - ) + addContentListRoute(ilRepository, contentClick) - ListsSection( - modifier = Modifier.padding(horizontal = MaterialTheme.paddings.baseline), - title = contentList.destinationTitle, - items = viewModel.data.collectAsLazyPagingItems(), - focusFirstItem = true, - scaleImageUrl = { imageUrl, containerWidth -> - viewModel.getScaledImageUrl(imageUrl, containerWidth) - }, - onItemClick = { item -> - when (item) { - is Content.Channel -> { - val nextContentList = when (contentList) { - is ContentList.RadioShows -> ContentList.RadioShowsForChannel( - bu = contentList.bu, - channelId = item.id, - channelTitle = item.title - ) - - is ContentList.RadioLatestMedias -> ContentList.RadioLatestMediasForChannel( - bu = contentList.bu, - channelId = item.id, - channelTitle = item.title - ) - - else -> error("Unsupported content list") - } + addContentListRoute(ilRepository, contentClick) - navController.navigate(nextContentList.destinationRoute) - } + addContentListRoute(ilRepository, contentClick) - is Content.Media -> { - val demoItem = DemoItem(title = item.title, uri = item.urn) + addContentListRoute(ilRepository, contentClick) - PlayerActivity.startPlayer(context, demoItem) - } + addContentListRoute(ilRepository, contentClick) - is Content.Show -> { - val show = ContentList.LatestMediaForShow( - urn = item.urn, - show = item.title, - ) + addContentListRoute(ilRepository, contentClick) - navController.navigate(show.destinationRoute) - } + addContentListRoute(ilRepository, contentClick) - is Content.Topic -> { - val topic = ContentList.LatestMediaForTopic( - urn = item.urn, - topic = item.title - ) + addContentListRoute(ilRepository, contentClick) - navController.navigate(topic.destinationRoute) - } - } - }, - emptyScreen = { emptyScreenModifier -> - Box( - modifier = emptyScreenModifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = stringResource(R.string.no_content)) - } - } - ) + addContentListRoute(ilRepository, contentClick) + + addContentListRoute(ilRepository, contentClick) + + addContentListRoute(ilRepository, contentClick) + + addContentListRoute(ilRepository, contentClick) + + addContentListRoute(ilRepository, contentClick) + } +} + +private inline fun NavGraphBuilder.addContentListRoute( + ilRepository: ILRepository, + crossinline onClick: (contentList: T, content: Content) -> Unit, +) { + composable { + val contentList = it.toRoute() + val viewModel = viewModel( + factory = ContentListViewModel.Factory( + ilRepository = ilRepository, + contentList = contentList, + ) + ) + + ListsSection( + modifier = Modifier.padding(horizontal = MaterialTheme.paddings.baseline), + title = contentList.destinationTitle, + items = viewModel.data.collectAsLazyPagingItems(), + focusFirstItem = true, + scaleImageUrl = { imageUrl, containerWidth -> + viewModel.getScaledImageUrl(imageUrl, containerWidth) + }, + onItemClick = { item -> + onClick(contentList, item) + }, + emptyScreen = { emptyScreenModifier -> + Box( + modifier = emptyScreenModifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = stringResource(R.string.no_content)) + } } - } + ) } } diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt index 098cb3d2e..d98f8c39c 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt @@ -135,7 +135,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( NavHost( navController = navController, - startDestination = SettingsRoutes.Main.route, + startDestination = SettingsRoutes.Main, modifier = modifier .focusRequester(focusRequester) .onFocusChanged { hasFocus = it.hasFocus } @@ -145,7 +145,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( } } ) { - composable(SettingsRoutes.Main.route) { + composable { val settings by settingsViewModel.settings.collectAsState() GenericSetting( @@ -158,7 +158,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( if (destination is SettingsRoutes.MetricsOverlay) { settingsViewModel.setMetricsOverlayEnabled(!destination.enabled) } else { - navController.navigate(destination.route) + navController.navigate(destination) } }, leadingContent = { setting -> @@ -182,7 +182,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( ) } - composable(SettingsRoutes.AudioTrack.route) { + composable { val audioTracks by settingsViewModel.audioTracks.collectAsState() audioTracks?.let { @@ -195,7 +195,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( } } - composable(SettingsRoutes.VideoTrack.route) { + composable { val videoTracks by settingsViewModel.videoTracks.collectAsState() videoTracks?.let { @@ -208,7 +208,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( } } - composable(SettingsRoutes.Subtitles.route) { + composable { val subtitles by settingsViewModel.subtitles.collectAsState() subtitles?.let { @@ -221,7 +221,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( } } - composable(SettingsRoutes.PlaybackSpeed.route) { + composable { val playbackSpeeds by settingsViewModel.playbackSpeeds.collectAsState() GenericSetting( @@ -243,7 +243,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( ) } - composable(SettingsRoutes.StatsForNerds.route) { + composable { if (player !is PillarboxExoPlayer) { return@composable } diff --git a/pillarbox-demo/build.gradle.kts b/pillarbox-demo/build.gradle.kts index 5041d0743..8271109b9 100644 --- a/pillarbox-demo/build.gradle.kts +++ b/pillarbox-demo/build.gradle.kts @@ -88,6 +88,7 @@ dependencies { implementation(libs.guava) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.core) implementation(libs.okhttp) implementation(libs.srg.data) implementation(libs.srg.dataprovider.retrofit) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt index 67c0013dc..344b25512 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt @@ -4,6 +4,7 @@ */ package ch.srgssr.pillarbox.demo +import androidx.compose.animation.AnimatedContentScope import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -37,11 +38,10 @@ import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController -import androidx.navigation.NavDeepLink import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost @@ -58,7 +58,7 @@ import ch.srgssr.pillarbox.demo.shared.di.PlayerModule import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination import ch.srgssr.pillarbox.demo.shared.ui.NavigationRoutes import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.SearchViewModel -import ch.srgssr.pillarbox.demo.shared.ui.navigate +import ch.srgssr.pillarbox.demo.shared.ui.navigateTopLevel import ch.srgssr.pillarbox.demo.shared.ui.settings.AppSettingsRepository import ch.srgssr.pillarbox.demo.shared.ui.settings.AppSettingsViewModel import ch.srgssr.pillarbox.demo.ui.examples.ExamplesHome @@ -74,11 +74,11 @@ import java.net.URL private val bottomNavItems = listOf(HomeDestination.Examples, HomeDestination.ShowCases, HomeDestination.Lists, HomeDestination.Search, HomeDestination.Settings) private val topLevelRoutes = listOf( - HomeDestination.Examples.route, - NavigationRoutes.showcaseList, - NavigationRoutes.contentLists, - HomeDestination.Search.route, - HomeDestination.Settings.route + NavigationRoutes.HomeSamples, + NavigationRoutes.ShowcaseList, + NavigationRoutes.ContentLists, + NavigationRoutes.SearchHome, + NavigationRoutes.SettingsHome, ) /** @@ -103,8 +103,8 @@ fun MainNavigation() { } }, navigationIcon = { - currentDestination?.route?.let { - if (!topLevelRoutes.contains(it)) { + currentDestination?.let { currentDestination -> + if (topLevelRoutes.none { currentDestination.hasRoute(it::class) }) { IconButton(onClick = { navController.navigateUp() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, @@ -115,7 +115,7 @@ fun MainNavigation() { } }, actions = { - if (currentDestination?.route == NavigationRoutes.contentLists) { + if (currentDestination?.hasRoute(NavigationRoutes.ContentLists::class) == true) { ListsMenu( currentServer = ilHost, onServerSelected = { ilHost = it } @@ -130,22 +130,22 @@ fun MainNavigation() { ) { innerPadding -> val context = LocalContext.current - NavHost(navController = navController, startDestination = HomeDestination.Examples.route, modifier = Modifier.padding(innerPadding)) { - composable(HomeDestination.Examples.route, DemoPageView("home", listOf("app", "pillarbox", "examples"))) { + NavHost(navController = navController, startDestination = NavigationRoutes.HomeSamples, modifier = Modifier.padding(innerPadding)) { + composable(DemoPageView("home", listOf("app", "pillarbox", "examples"))) { ExamplesHome() } - navigation(startDestination = NavigationRoutes.showcaseList, route = HomeDestination.ShowCases.route) { + navigation(NavigationRoutes.ShowcaseList) { showcasesNavGraph(navController) } - navigation(startDestination = NavigationRoutes.contentLists, route = HomeDestination.Lists.route) { + navigation(NavigationRoutes.ContentLists) { val ilRepository = PlayerModule.createIlRepository(context, ilHost) listsNavGraph(navController, ilRepository, ilHost) } - composable(route = HomeDestination.Settings.route, DemoPageView("home", listOf("app", "pillarbox", "settings"))) { + composable(DemoPageView("home", listOf("app", "pillarbox", "settings"))) { val appSettingsRepository = remember(context) { AppSettingsRepository(context) } @@ -154,7 +154,7 @@ fun MainNavigation() { AppSettingsView(appSettingsViewModel) } - composable(route = NavigationRoutes.searchHome, DemoPageView("home", listOf("app", "pillarbox", "search"))) { + composable(DemoPageView("home", listOf("app", "pillarbox", "search"))) { val ilRepository = PlayerModule.createIlRepository(context) val viewModel: SearchViewModel = viewModel(factory = SearchViewModel.Factory(ilRepository)) SearchHome(searchViewModel = viewModel) { @@ -240,9 +240,9 @@ private fun DemoBottomNavigation(navController: NavController, currentDestinatio Icon(imageVector = screen.imageVector, contentDescription = null) }, label = { Text(stringResource(screen.labelResId)) }, - selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true, + selected = currentDestination?.hierarchy?.any { it.hasRoute(screen.route::class) } == true, onClick = { - navController.navigate(screen) + navController.navigateTopLevel(screen.route) } ) } @@ -272,29 +272,17 @@ private fun DemoBottomNavigationPreview() { } private fun NavDestination.getLabelResId(): Int { - val routes = hierarchy.map { it.route } - val navItem: HomeDestination? = bottomNavItems.firstOrNull { it.route in routes } + val navItem: HomeDestination? = bottomNavItems.firstOrNull { destination -> + hierarchy.any { it.hasRoute(destination.route::class) } + } return navItem?.labelResId ?: ResourcesCompat.ID_NULL } -/** - * Add the Composable to the NavGraphBuilder - * - * @param route route for the destination - * @param pageView page view to send to [SRGPageViewTracker] - * @param arguments list of arguments to associate with destination - * @param deepLinks list of deep links to associate with the destinations - * @param content composable for the destination - * @receiver - */ -fun NavGraphBuilder.composable( - route: String, +internal inline fun NavGraphBuilder.composable( pageView: DemoPageView, - arguments: List = emptyList(), - deepLinks: List = emptyList(), - content: @Composable (NavBackStackEntry) -> Unit + noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { - composable(route = route, arguments = arguments, deepLinks = deepLinks) { + composable { LifecycleEventEffect(Lifecycle.Event.ON_RESUME, it) { SRGAnalytics.trackPagView(pageView) } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt index dc581f94f..0c6e1e8f5 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt @@ -12,13 +12,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder +import androidx.navigation.toRoute import androidx.paging.compose.collectAsLazyPagingItems import ch.srgssr.pillarbox.demo.DemoPageView import ch.srgssr.pillarbox.demo.composable @@ -28,7 +28,6 @@ import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.ContentList import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.ContentListViewModel import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.Content import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository -import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListFactories import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListSections import ch.srgssr.pillarbox.demo.ui.components.DemoListHeaderView import ch.srgssr.pillarbox.demo.ui.components.DemoListItemView @@ -52,7 +51,7 @@ fun NavGraphBuilder.listsNavGraph(navController: NavController, ilRepository: IL show = content.title ) - navController.navigate(nextContentList.destinationRoute) + navController.navigate(nextContentList) } is Content.Topic -> { @@ -61,7 +60,7 @@ fun NavGraphBuilder.listsNavGraph(navController: NavController, ilRepository: IL topic = content.title ) - navController.navigate(nextContentList.destinationRoute) + navController.navigate(nextContentList) } is Content.Media -> { @@ -86,41 +85,64 @@ fun NavGraphBuilder.listsNavGraph(navController: NavController, ilRepository: IL else -> error("Unsupported content list") } - navController.navigate(nextContentList.destinationRoute) + navController.navigate(nextContentList) } } } - composable(route = NavigationRoutes.contentLists, DemoPageView("home", defaultListsLevels)) { + composable(DemoPageView("home", defaultListsLevels)) { ListsHome { contentList -> - navController.navigate(route = contentList.destinationRoute) + navController.navigate(contentList) } } - contentListFactories.forEach { contentListFactory -> - composable( - route = contentListFactory.route, - pageView = DemoPageView(contentListFactory.trackerTitle, defaultListsLevels) - ) { navBackStackEntry -> - val contentList = contentListFactory.parse(navBackStackEntry) - val viewModel = viewModel( - factory = ContentListViewModel.Factory( - ilRepository = ilRepository, - contentList = contentList, - ) - ) + addContentListRoute(trackerTitle = "tv-topics", ilRepository, contentClick) - ListsSubSection( - title = contentList.destinationTitle, - items = viewModel.data.collectAsLazyPagingItems(), - modifier = Modifier.fillMaxWidth(), - contentClick = { contentClick(contentList, it) } - ) - } - } + addContentListRoute(trackerTitle = "tv-shows", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "tv-latest-videos", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "tv-livestreams", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "live-center", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "live-web", ilRepository, contentClick) - composable("content/error", DemoPageView("error", defaultListsLevels)) { - Text(text = "Cannot find content!") + addContentListRoute(trackerTitle = "radio-livestreams", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "latest-audios", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "shows", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "latest-media-for-show", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "latest-media-for-topic", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "shows-for-channel", ilRepository, contentClick) + + addContentListRoute(trackerTitle = "latest-audios-for-channel", ilRepository, contentClick) +} + +private inline fun NavGraphBuilder.addContentListRoute( + trackerTitle: String, + ilRepository: ILRepository, + crossinline onClick: (contentList: T, content: Content) -> Unit, +) { + composable(pageView = DemoPageView(trackerTitle, defaultListsLevels)) { navBackStackEntry -> + val contentList = navBackStackEntry.toRoute() + val viewModel = viewModel( + factory = ContentListViewModel.Factory( + ilRepository = ilRepository, + contentList = contentList, + ) + ) + + ListsSubSection( + title = contentList.destinationTitle, + items = viewModel.data.collectAsLazyPagingItems(), + modifier = Modifier.fillMaxWidth(), + contentClick = { onClick(contentList, it) } + ) } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt index c6f35eb61..5e8f83a34 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt @@ -50,9 +50,8 @@ fun PlaybackSettingsContent( val navController = rememberNavController() val settingsViewModel: PlayerSettingsViewModel = viewModel(factory = PlayerSettingsViewModel.Factory(player, application)) Surface(modifier = modifier) { - NavHost(navController = navController, startDestination = SettingsRoutes.Main.route) { - composable( - route = SettingsRoutes.Main.route, + NavHost(navController = navController, startDestination = SettingsRoutes.Main) { + composable( exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Down) }, @@ -69,7 +68,7 @@ fun PlaybackSettingsContent( if (destination is SettingsRoutes.MetricsOverlay) { settingsViewModel.setMetricsOverlayEnabled(!destination.enabled) } else { - navController.navigate(destination.route) { + navController.navigate(destination) { launchSingleTop = true } } @@ -77,8 +76,7 @@ fun PlaybackSettingsContent( ) } - composable( - route = SettingsRoutes.PlaybackSpeed.route, + composable( exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Down) }, @@ -93,8 +91,7 @@ fun PlaybackSettingsContent( ) } - composable( - route = SettingsRoutes.Subtitles.route, + composable( exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Down) }, @@ -113,8 +110,7 @@ fun PlaybackSettingsContent( } } - composable( - route = SettingsRoutes.AudioTrack.route, + composable( exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Down) }, @@ -133,8 +129,7 @@ fun PlaybackSettingsContent( } } - composable( - route = SettingsRoutes.VideoTrack.route, + composable( exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Down) }, @@ -153,8 +148,7 @@ fun PlaybackSettingsContent( } } - composable( - route = SettingsRoutes.StatsForNerds.route, + composable( exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Down) }, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt index 0500468c8..2e51ea031 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt @@ -67,7 +67,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.simple_player), modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.simplePlayer) } + onClick = { navController.navigate(NavigationRoutes.SimplePlayer) } ) HorizontalDivider() @@ -75,7 +75,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.story), modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.story) } + onClick = { navController.navigate(NavigationRoutes.Story) } ) HorizontalDivider() @@ -83,11 +83,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.chapters), modifier = itemModifier, - onClick = { - navController.navigate( - NavigationRoutes.chapters - ) - } + onClick = { navController.navigate(NavigationRoutes.Chapters) } ) } @@ -111,7 +107,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.showcase_playback_settings), modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.showcasePlaybackSettings) }, + onClick = { navController.navigate(NavigationRoutes.ShowcasePlaybackSettings) }, ) } @@ -124,7 +120,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.exoplayer_view), modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.exoPlayerSample) } + onClick = { navController.navigate(NavigationRoutes.ExoPlayerSample) } ) HorizontalDivider() @@ -148,11 +144,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.start_given_time_example), modifier = itemModifier, - onClick = { - navController.navigate( - NavigationRoutes.startAtGivenTime - ) - } + onClick = { navController.navigate(NavigationRoutes.StartAtGivenTime) } ) HorizontalDivider() @@ -160,7 +152,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.adaptive), modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.adaptive) } + onClick = { navController.navigate(NavigationRoutes.Adaptive) } ) HorizontalDivider() @@ -168,14 +160,14 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.player_swap), modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.playerSwap) } + onClick = { navController.navigate(NavigationRoutes.PlayerSwap) } ) HorizontalDivider() DemoListItemView( title = stringResource(R.string.tracker_example), modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.trackingSample) } + onClick = { navController.navigate(NavigationRoutes.TrackingSample) } ) HorizontalDivider() @@ -183,11 +175,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.update_media_item_example), modifier = itemModifier, - onClick = { - navController.navigate( - NavigationRoutes.updatableSample - ) - } + onClick = { navController.navigate(NavigationRoutes.UpdatableSample) } ) HorizontalDivider() @@ -195,11 +183,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.smooth_seeking_example), modifier = itemModifier, - onClick = { - navController.navigate( - NavigationRoutes.smoothSeeking - ) - } + onClick = { navController.navigate(NavigationRoutes.SmoothSeeking) } ) HorizontalDivider() @@ -207,11 +191,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.video_360), modifier = itemModifier, - onClick = { - navController.navigate( - NavigationRoutes.video360 - ) - } + onClick = { navController.navigate(NavigationRoutes.Video360) } ) } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt index 0a5cf7267..f9dcd46ab 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt @@ -27,43 +27,43 @@ import ch.srgssr.pillarbox.demo.ui.showcases.playlists.CustomPlaybackSettingsSho * Inject Showcases Navigation */ fun NavGraphBuilder.showcasesNavGraph(navController: NavController) { - composable(NavigationRoutes.showcaseList, DemoPageView("home", Levels)) { + composable(DemoPageView("home", Levels)) { ShowcasesHome(navController = navController) } - composable(NavigationRoutes.showcasePlaybackSettings, DemoPageView("playback settings", Levels)) { + composable(DemoPageView("playback settings", Levels)) { CustomPlaybackSettingsShowcase(playlist = Playlist.VideoUrls) } - composable(NavigationRoutes.story, DemoPageView("story", Levels)) { + composable(DemoPageView("story", Levels)) { StoryLayoutShowcase() } - composable(NavigationRoutes.simplePlayer, DemoPageView("basic player", Levels)) { + composable(DemoPageView("basic player", Levels)) { SimpleLayoutShowcase() } - composable(NavigationRoutes.adaptive, DemoPageView("adaptive player", Levels)) { + composable(DemoPageView("adaptive player", Levels)) { ResizablePlayerShowcase() } - composable(NavigationRoutes.playerSwap, DemoPageView("multiplayer", Levels)) { + composable(DemoPageView("multiplayer", Levels)) { MultiPlayerShowcase() } - composable(NavigationRoutes.exoPlayerSample, DemoPageView("exoplayer", Levels)) { + composable(DemoPageView("exoplayer", Levels)) { ExoPlayerShowcase() } - composable(NavigationRoutes.trackingSample, DemoPageView("tracking toggle", Levels)) { + composable(DemoPageView("tracking toggle", Levels)) { TrackingToggleShowcase() } - composable(NavigationRoutes.updatableSample, DemoPageView("updatable item", Levels)) { + composable(DemoPageView("updatable item", Levels)) { UpdatableMediaItemShowcase() } - composable(NavigationRoutes.smoothSeeking, DemoPageView("smooth seeking", Levels)) { + composable(DemoPageView("smooth seeking", Levels)) { SmoothSeekingShowcase() } - composable(NavigationRoutes.startAtGivenTime, DemoPageView("start at given time", Levels)) { + composable(DemoPageView("start at given time", Levels)) { StartAtGivenTimeShowcase() } - composable(NavigationRoutes.video360, DemoPageView("Video 360°", Levels)) { + composable(DemoPageView("Video 360°", Levels)) { SphericalSurfaceShowcase() } - composable(NavigationRoutes.chapters, DemoPageView("Chapters", Levels)) { + composable(DemoPageView("Chapters", Levels)) { ChapterShowcase() } }