Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a menu to change environment in "Lists" on mobile #358

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,26 @@ import java.net.URL
*/
object PlayerModule {

private fun provideIntegrationLayerItemSource(context: Context): MediaCompositionMediaItemSource =
private fun provideIntegrationLayerItemSource(context: Context, ilHost: URL = IlHost.DEFAULT): MediaCompositionMediaItemSource =
MediaCompositionMediaItemSource(
mediaCompositionDataSource = DefaultMediaCompositionDataSource(vector = context.getVector()),
mediaCompositionDataSource = DefaultMediaCompositionDataSource(vector = context.getVector(), baseUrl = ilHost),
)

/**
* Provide mixed item source that load Url and Urn
*/
fun provideMixedItemSource(context: Context): MixedMediaItemSource = MixedMediaItemSource(
provideIntegrationLayerItemSource(context)
fun provideMixedItemSource(
context: Context,
ilHost: URL = IlHost.DEFAULT
): MixedMediaItemSource = MixedMediaItemSource(
provideIntegrationLayerItemSource(context, ilHost)
)

/**
* Provide default player that allow to play urls and urns content from the SRG
*/
fun provideDefaultPlayer(context: Context): PillarboxPlayer {
return DefaultPillarbox(context = context, mediaItemSource = provideMixedItemSource(context))
fun provideDefaultPlayer(context: Context, ilHost: URL = IlHost.DEFAULT): PillarboxPlayer {
return DefaultPillarbox(context = context, mediaItemSource = provideMixedItemSource(context, ilHost))
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ package ch.srgssr.pillarbox.demo.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
Expand All @@ -18,10 +24,16 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
Expand All @@ -42,7 +54,9 @@ import androidx.navigation.navigation
import ch.srg.dataProvider.integrationlayer.request.image.ImageWidth
import ch.srg.dataProvider.integrationlayer.request.image.decorated
import ch.srgssr.pillarbox.analytics.SRGAnalytics
import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost
import ch.srgssr.pillarbox.demo.DemoPageView
import ch.srgssr.pillarbox.demo.R
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination
Expand All @@ -54,10 +68,13 @@ import ch.srgssr.pillarbox.demo.ui.integrationLayer.SearchView
import ch.srgssr.pillarbox.demo.ui.integrationLayer.listNavGraph
import ch.srgssr.pillarbox.demo.ui.player.SimplePlayerActivity
import ch.srgssr.pillarbox.demo.ui.showcases.showCasesNavGraph
import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme
import ch.srgssr.pillarbox.demo.ui.theme.paddings
import java.net.URL

private val bottomNavItems = listOf(HomeDestination.Examples, HomeDestination.ShowCases, HomeDestination.Lists, HomeDestination.Search)
private val topLevelRoutes =
listOf(HomeDestination.Examples.route, NavigationRoutes.showcaseList, NavigationRoutes.contentLists, HomeDestination.Search)
listOf(HomeDestination.Examples.route, NavigationRoutes.showcaseList, NavigationRoutes.contentLists, HomeDestination.Search.route)

/**
* Main view with all the navigation
Expand All @@ -69,6 +86,9 @@ fun MainNavigation() {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination

var ilHost by remember { mutableStateOf(IlHost.DEFAULT) }

Scaffold(
topBar = {
TopAppBar(
Expand All @@ -88,6 +108,14 @@ fun MainNavigation() {
}
}
}
},
actions = {
if (currentDestination?.route == NavigationRoutes.contentLists) {
ListsMenu(
currentServer = ilHost,
onServerSelected = { ilHost = it }
)
}
}
)
},
Expand All @@ -96,9 +124,7 @@ fun MainNavigation() {
}
) { innerPadding ->
val context = LocalContext.current
val ilRepository = remember {
PlayerModule.createIlRepository(context)
}

NavHost(navController = navController, startDestination = HomeDestination.Examples.route, modifier = Modifier.padding(innerPadding)) {
composable(HomeDestination.Examples.route, DemoPageView("home", listOf("app", "pillarbox", "examples"))) {
ExamplesHome()
Expand All @@ -109,14 +135,17 @@ fun MainNavigation() {
}

navigation(startDestination = NavigationRoutes.contentLists, route = HomeDestination.Lists.route) {
listNavGraph(navController, ilRepository)
val ilRepository = PlayerModule.createIlRepository(context, ilHost)

listNavGraph(navController, ilRepository, ilHost)
}

composable(HomeDestination.Info.route, DemoPageView("home", listOf("app", "pillarbox", "information"))) {
InfoView()
}

composable(route = NavigationRoutes.searchHome, DemoPageView("home", listOf("app", "pillarbox", "search"))) {
val ilRepository = PlayerModule.createIlRepository(context)
val viewModel: SearchViewModel = viewModel(factory = SearchViewModel.Factory(ilRepository))
SearchView(searchViewModel = viewModel) {
val item = DemoItem(
Expand All @@ -133,6 +162,65 @@ fun MainNavigation() {
}
}

@Composable
private fun ListsMenu(
currentServer: URL,
onServerSelected: (server: URL) -> Unit
) {
var isMenuVisible by remember { mutableStateOf(false) }

IconButton(onClick = { isMenuVisible = true }) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.server)
)
}

DropdownMenu(
expanded = isMenuVisible,
onDismissRequest = { isMenuVisible = false },
offset = DpOffset(
x = MaterialTheme.paddings.small,
y = 0.dp,
),
) {
val currentServerUrl = currentServer.toString()
val servers = mapOf(
stringResource(R.string.production) to IlHost.PROD.toString(),
stringResource(R.string.stage) to IlHost.STAGE.toString(),
stringResource(R.string.test) to IlHost.TEST.toString()
)

Text(
text = stringResource(R.string.server),
modifier = Modifier
.padding(MenuDefaults.DropdownMenuItemContentPadding)
.align(Alignment.CenterHorizontally),
style = MaterialTheme.typography.labelMedium
)

servers.forEach { (name, url) ->
DropdownMenuItem(
text = { Text(text = name) },
onClick = {
onServerSelected(URL(url))
isMenuVisible = false
},
trailingIcon = if (currentServerUrl == url) {
{
Icon(
imageVector = Icons.Default.Check,
contentDescription = null
)
}
} else {
null
}
)
}
}
}

@Composable
private fun DemoBottomNavigation(navController: NavController, currentDestination: NavDestination?) {
NavigationBar {
Expand Down Expand Up @@ -163,6 +251,28 @@ private fun DemoBottomNavigation(navController: NavController, currentDestinatio
}
}

@Composable
@Preview(showBackground = true)
private fun ListsMenuPreview() {
PillarboxTheme {
ListsMenu(
currentServer = IlHost.PROD,
onServerSelected = {}
)
}
}

@Composable
@Preview(showBackground = true)
private fun DemoBottomNavigationPreview() {
PillarboxTheme {
DemoBottomNavigation(
navController = rememberNavController(),
currentDestination = null
)
}
}

private fun NavDestination.getLabelResId(): Int {
val routes = hierarchy.map { it.route }
val navItem: HomeDestination? = bottomNavItems.firstOrNull { it.route in routes }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ import ch.srgssr.pillarbox.demo.ui.composable
import ch.srgssr.pillarbox.demo.ui.player.SimplePlayerActivity
import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme
import ch.srgssr.pillarbox.demo.ui.theme.paddings
import java.net.URL

private val defaultListsLevels = listOf("app", "pillarbox", "lists")

/**
* Build Navigation for integration layer list view
*/
fun NavGraphBuilder.listNavGraph(navController: NavController, ilRepository: ILRepository) {
fun NavGraphBuilder.listNavGraph(navController: NavController, ilRepository: ILRepository, ilHost: URL) {
val contentClick = { contentList: ContentList, content: Content ->
when (content) {
is Content.Show -> {
Expand All @@ -65,7 +66,7 @@ fun NavGraphBuilder.listNavGraph(navController: NavController, ilRepository: ILR

is Content.Media -> {
val item = DemoItem(title = content.title, uri = content.urn)
SimplePlayerActivity.startActivity(navController.context, item)
SimplePlayerActivity.startActivity(navController.context, item, ilHost)
}

is Content.Channel -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import android.os.Bundle
import android.os.IBinder
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
Expand All @@ -24,10 +23,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.Player
import ch.srgssr.pillarbox.analytics.SRGAnalytics
import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost
import ch.srgssr.pillarbox.demo.DemoPageView
import ch.srgssr.pillarbox.demo.service.DemoPlaybackService
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
Expand All @@ -37,6 +38,7 @@ import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme
import ch.srgssr.pillarbox.player.service.PlaybackService
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.net.URL

/**
* Simple player activity that can handle picture in picture.
Expand All @@ -49,7 +51,7 @@ import kotlinx.coroutines.launch
*/
class SimplePlayerActivity : ComponentActivity(), ServiceConnection {

private val playerViewModel: SimplePlayerViewModel by viewModels()
private lateinit var playerViewModel: SimplePlayerViewModel
private var layoutStyle: Int = LAYOUT_PLAYLIST

private fun readIntent(intent: Intent) {
Expand All @@ -66,6 +68,8 @@ class SimplePlayerActivity : ComponentActivity(), ServiceConnection {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val ilHost = (intent.extras?.getSerializable(ARG_IL_HOST) as URL?) ?: IlHost.DEFAULT
playerViewModel = ViewModelProvider(this, factory = SimplePlayerViewModel.Factory(application, ilHost))[SimplePlayerViewModel::class.java]
readIntent(intent)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
Expand Down Expand Up @@ -185,25 +189,27 @@ class SimplePlayerActivity : ComponentActivity(), ServiceConnection {
companion object {
private const val ARG_PLAYLIST = "ARG_PLAYLIST"
private const val ARG_LAYOUT = "ARG_LAYOUT"
private const val ARG_IL_HOST = "ARG_IL_HOST"
private const val LAYOUT_SIMPLE = 1
private const val LAYOUT_PLAYLIST = 0

/**
* Start activity [SimplePlayerActivity] with [playlist]
*/
fun startActivity(context: Context, playlist: Playlist) {
fun startActivity(context: Context, playlist: Playlist, ilHost: URL = IlHost.DEFAULT) {
val layoutStyle: Int = if (playlist.items.isEmpty() || playlist.items.size > 1) LAYOUT_PLAYLIST else LAYOUT_SIMPLE
val intent = Intent(context, SimplePlayerActivity::class.java)
intent.putExtra(ARG_PLAYLIST, playlist)
intent.putExtra(ARG_LAYOUT, layoutStyle)
intent.putExtra(ARG_IL_HOST, ilHost)
context.startActivity(intent)
}

/**
* Start activity [SimplePlayerActivity] with [demoItem]
* Start activity [SimplePlayerActivity] with DemoItem.
*/
fun startActivity(context: Context, item: DemoItem) {
startActivity(context, Playlist("UniqueItem", listOf(item)))
fun startActivity(context: Context, item: DemoItem, ilHost: URL = IlHost.DEFAULT) {
startActivity(context, Playlist("UniqueItem", listOf(item)), ilHost)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import android.app.Application
import android.util.Log
import android.util.Rational
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.media3.common.C
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
Expand All @@ -20,15 +22,16 @@ import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.player.extension.toRational
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.net.URL

/**
* Simple player view model than handle a PillarboxPlayer [player]
*/
class SimplePlayerViewModel(application: Application) : AndroidViewModel(application), Player.Listener {
class SimplePlayerViewModel(application: Application, ilHost: URL) : AndroidViewModel(application), Player.Listener {
/**
* Player as PillarboxPlayer
*/
val player = PlayerModule.provideDefaultPlayer(application)
val player = PlayerModule.provideDefaultPlayer(application, ilHost)

private val _pauseOnBackground = MutableStateFlow(true)
private val _displayNotification = MutableStateFlow(false)
Expand Down Expand Up @@ -176,4 +179,13 @@ class SimplePlayerViewModel(application: Application) : AndroidViewModel(applica
companion object {
private const val TAG = "PillarboxDemo"
}

/**
* Factory to create [SimplePlayerViewModel].
*/
class Factory(private val application: Application, private val ilHost: URL) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SimplePlayerViewModel(application, ilHost) as T
}
}
}
4 changes: 4 additions & 0 deletions pillarbox-demo/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@
<string name="clear">Clear</string>
<string name="licence_url">License URL</string>
<string name="play">Play</string>
<string name="server">Server</string>
<string name="production">Production</string>
<string name="stage">Stage</string>
<string name="test">Test</string>
</resources>