diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/ErrorView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/ErrorView.kt new file mode 100644 index 000000000..58dacc65f --- /dev/null +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/ErrorView.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.tv.player.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.media3.common.PlaybackException +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.Text +import ch.srgssr.pillarbox.core.business.SRGErrorMessageProvider + +/** + * Player error + * + * @param playerError The player error. + * @param modifier The modifier to layout the view. + * @param onRetry Action to retry. + * @receiver + */ +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun PlayerError(playerError: PlaybackException, modifier: Modifier = Modifier, onRetry: () -> Unit) { + val context = LocalContext.current + val errorMessageProvider = remember(context) { + SRGErrorMessageProvider(context) + } + Box( + modifier = Modifier + .fillMaxSize() + .clickable { onRetry.invoke() } + ) { + Column(modifier = Modifier.align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = errorMessageProvider.getErrorMessage(playerError).second, + color = Color.White + ) + Text(text = "Click to retry!", color = Color.LightGray, fontStyle = FontStyle.Italic) + } + } +} + +@Preview +@Composable +private fun PlayerErrorPreview() { + PlayerError(playerError = PlaybackException("", null, PlaybackException.ERROR_CODE_UNSPECIFIED)) { + // Nothing + } +} diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt index 082b7e4ca..41a2a7d71 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/player/compose/TvPlayerView.kt @@ -15,7 +15,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -24,11 +26,12 @@ import androidx.tv.material3.DrawerValue import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.IconButton +import androidx.tv.material3.LocalContentColor import androidx.tv.material3.MaterialTheme import androidx.tv.material3.rememberDrawerState -import ch.srgssr.pillarbox.demo.tv.R import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings import ch.srgssr.pillarbox.ui.extension.handleDPadKeyEvents +import ch.srgssr.pillarbox.ui.extension.playerErrorAsState import ch.srgssr.pillarbox.ui.widget.maintainVisibleOnFocus import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface import ch.srgssr.pillarbox.ui.widget.rememberDelayedVisibilityState @@ -60,46 +63,55 @@ fun TvPlayerView( drawerState = drawerState, modifier = modifier ) { - PlayerSurface( - player = player, - modifier = Modifier - .fillMaxSize() - .handleDPadKeyEvents(onEnter = { - visibilityState.show() - }) - .focusable(true) - ) - AnimatedVisibility( - visible = visibilityState.isVisible, - enter = expandVertically { it }, - exit = shrinkVertically { it } - ) { - Box( + val error by player.playerErrorAsState() + if (error != null) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onSurface + ) { + PlayerError(modifier = Modifier.fillMaxSize(), playerError = error!!, onRetry = player::prepare) + } + } else { + PlayerSurface( + player = player, modifier = Modifier .fillMaxSize() - .maintainVisibleOnFocus(delayedVisibilityState = visibilityState), - contentAlignment = Alignment.Center + .handleDPadKeyEvents(onEnter = { + visibilityState.show() + }) + .focusable(true) + ) + AnimatedVisibility( + visible = visibilityState.isVisible, + enter = expandVertically { it }, + exit = shrinkVertically { it } ) { - TvPlaybackRow( - player = player, - state = visibilityState - ) - - IconButton( - onClick = { drawerState.setValue(DrawerValue.Open) }, + Box( modifier = Modifier - .padding(MaterialTheme.paddings.baseline) - .align(Alignment.BottomEnd) + .fillMaxSize() + .maintainVisibleOnFocus(delayedVisibilityState = visibilityState), + contentAlignment = Alignment.Center ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = stringResource(R.string.settings) + TvPlaybackRow( + player = player, + state = visibilityState ) + + IconButton( + onClick = { drawerState.setValue(DrawerValue.Open) }, + modifier = Modifier + .padding(MaterialTheme.paddings.baseline) + .align(Alignment.BottomEnd) + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = stringResource(ch.srgssr.pillarbox.demo.tv.R.string.settings) + ) + } } } - } - BackHandler(enabled = visibilityState.isVisible) { - visibilityState.hide() + BackHandler(enabled = visibilityState.isVisible) { + visibilityState.hide() + } } } }