Skip to content
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 @@ -15,7 +15,9 @@ import androidx.compose.material.icons.filled.Tune
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionParameters
Expand Down Expand Up @@ -342,14 +344,15 @@ class PlayerSettingsViewModel(
* Factory
*
* @param player
* @param application
* @constructor Create an empty Factory
*/
class Factory(
private val player: Player,
private val application: Application
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val application = checkNotNull(extras[APPLICATION_KEY])

@Suppress("UNCHECKED_CAST")
return PlayerSettingsViewModel(player, application) as T
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
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.collectAsState
import androidx.compose.runtime.getValue
Expand All @@ -33,22 +37,27 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.Timeline.Window
import androidx.tv.material3.Button
import androidx.tv.material3.DrawerValue
import androidx.tv.material3.Icon
import androidx.tv.material3.IconButton
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.ModalNavigationDrawer
import androidx.tv.material3.Text
import androidx.tv.material3.rememberDrawerState
import ch.srgssr.pillarbox.demo.shared.R
import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent
import ch.srgssr.pillarbox.demo.shared.ui.components.PillarboxSlider
import ch.srgssr.pillarbox.demo.shared.ui.getFormatter
Expand All @@ -58,8 +67,10 @@ import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay
import ch.srgssr.pillarbox.demo.shared.ui.player.rememberDelayedControlsVisibility
import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTalkBackEnabled
import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions
import ch.srgssr.pillarbox.demo.tv.R
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerError
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerPlaybackRow
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.playlist.PlaylistDrawer
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.settings.PlaybackSettingsDrawer
import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
Expand All @@ -84,6 +95,12 @@ import kotlinx.datetime.toLocalDateTime
import kotlin.time.Duration.Companion.ZERO
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import ch.srgssr.pillarbox.demo.shared.R as shareR

private enum class DrawerMode {
PLAYLIST,
SETTINGS,
}

/**
* TV player view
Expand All @@ -103,12 +120,13 @@ fun PlayerView(
metricsOverlayOptions: MetricsOverlayOptions,
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)

val talkBackEnabled = rememberIsTalkBackEnabled()
val isPlaying by player.isPlayingAsState()
val keepControlDelay = if (!talkBackEnabled && isPlaying) DefaultVisibilityDelay else ZERO
val controlsVisibilityState = rememberDelayedControlsVisibility(initialVisible = true, keepControlDelay)

var drawerMode by remember { mutableStateOf(DrawerMode.SETTINGS) }

LaunchedEffect(drawerState.currentValue) {
controlsVisibilityState.visible = when (drawerState.currentValue) {
DrawerValue.Closed -> true
Expand All @@ -120,10 +138,42 @@ fun PlayerView(
controlsVisibilityState.visible = false
}

PlaybackSettingsDrawer(
player = player,
drawerState = drawerState,
ModalNavigationDrawer(
drawerContent = { drawerValue ->
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
if (drawerValue == DrawerValue.Open) {
BackHandler {
drawerState.setValue(DrawerValue.Closed)
}

var hasFocus by remember { mutableStateOf(false) }

val focusRequester = remember { FocusRequester() }
val modifier = Modifier
.width(320.dp)
.fillMaxHeight()
.padding(MaterialTheme.paddings.baseline)
.background(
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f),
shape = MaterialTheme.shapes.large
)
.focusRequester(focusRequester)
.onFocusChanged { hasFocus = it.hasFocus }
.onGloballyPositioned {
if (!hasFocus) {
focusRequester.requestFocus()
}
}

when (drawerMode) {
DrawerMode.PLAYLIST -> PlaylistDrawer(player, modifier)
DrawerMode.SETTINGS -> PlaybackSettingsDrawer(player, modifier)
}
}
}
},
modifier = modifier,
drawerState = drawerState,
) {
val error by player.playerErrorAsState()
if (error != null) {
Expand Down Expand Up @@ -181,6 +231,7 @@ fun PlayerView(
onClick = { player.seekTo(currentCredit?.end ?: 0L) },
)
}

AnimatedVisibility(
visible = controlsVisibilityState.visible,
modifier = Modifier
Expand Down Expand Up @@ -211,13 +262,32 @@ fun PlayerView(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
IconButton(
onClick = { drawerState.setValue(DrawerValue.Open) },
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.baseline),
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.settings),
)
IconButton(
onClick = {
drawerMode = DrawerMode.SETTINGS
drawerState.setValue(DrawerValue.Open)
},
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(shareR.string.settings),
)
}

IconButton(
onClick = {
drawerMode = DrawerMode.PLAYLIST
drawerState.setValue(DrawerValue.Open)
},
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.PlaylistPlay,
contentDescription = stringResource(R.string.playlist),
)
}
}

if (currentCredit != null) {
Expand Down Expand Up @@ -294,7 +364,7 @@ private fun SkipButton(
onClick = onClick,
modifier = modifier,
) {
Text(text = stringResource(R.string.skip))
Text(text = stringResource(shareR.string.skip))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.demo.tv.ui.player.compose.playlist

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.media3.common.Player
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.NavigationDrawerItem
import androidx.tv.material3.NavigationDrawerScope
import androidx.tv.material3.Text
import ch.srgssr.pillarbox.demo.tv.R
import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings
import ch.srgssr.pillarbox.ui.extension.currentMediaItemIndexAsState
import ch.srgssr.pillarbox.ui.extension.getCurrentMediaItemsAsState
import coil3.compose.AsyncImage

/**
* Drawer used to display a player's playlist.
*
* @param player The currently active player.
* @param modifier The [Modifier] to apply to this layout.
*/
@Composable
fun NavigationDrawerScope.PlaylistDrawer(
player: Player,
modifier: Modifier = Modifier,
) {
val mediaItems by player.getCurrentMediaItemsAsState()
val currentMediaItemIndex by player.currentMediaItemIndexAsState()

Column(
modifier = modifier
.padding(horizontal = MaterialTheme.paddings.baseline)
.padding(top = MaterialTheme.paddings.baseline),
) {
Text(
text = stringResource(R.string.playlist),
style = MaterialTheme.typography.titleMedium
)

LazyColumn(
contentPadding = PaddingValues(vertical = MaterialTheme.paddings.baseline),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.baseline)
) {
itemsIndexed(mediaItems) { index, mediaItem ->
NavigationDrawerItem(
selected = index == currentMediaItemIndex,
onClick = {
player.seekToDefaultPosition(index)
player.play()
},
leadingContent = {
AsyncImage(
model = mediaItem.mediaMetadata.artworkUri,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
)
},
supportingContent = if (mediaItem.mediaMetadata.description != null) {
{
Text(
text = mediaItem.mediaMetadata.description.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
} else {
null
},
content = {
Text(
text = mediaItem.mediaMetadata.title.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
)
}
}
}
}
Loading
Loading