@AndroidEntryPoint
class StoriesPlayerV2 : Fragment() {
var player by mutableStateOf<MediaController?>(null)
val viewModel by viewModels<StoriesPlayerViewModel2>()
@Inject
lateinit var controllerFuture: ListenableFuture<MediaController>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
player?.let {
PlayerScreen(Modifier.fillMaxWidth(1f), it)
}
}
}
}
override fun onStart() {
controllerFuture.addListener(
{
player = controllerFuture.get()
lifecycleScope.launch {
val newPlaylist = withContext(Dispatchers.IO) {
if (!viewModel.initialized) {
viewModel.initialized = true
viewModel.newPlaylist()
} else {
listOf()
}
}
if (newPlaylist.isNotEmpty()) {
player?.setMediaItems(newPlaylist)
player?.prepare()
player?.seekTo(viewModel.listPosition, viewModel.lastPlayedPosition)
player?.play()
}
}
},
MoreExecutors.directExecutor()
)
super.onStart()
}
override fun onStop() {
super.onStop()
player?.release()
MediaController.releaseFuture(controllerFuture)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PlayerScreen(modifier: Modifier, player: MediaController) {
var position by remember { mutableStateOf(0L) }
var sliderPosition: Long? by remember {
mutableStateOf(null)
}
var duration by remember { mutableStateOf(0L) }
var mediaMetadataState by remember { mutableStateOf<MediaMetadata?>(player.mediaMetadata) }
var currentMediaItem by remember {
mutableStateOf(player.currentMediaItem)
}
var isPlayingState by remember {
mutableStateOf(player.isPlaying)
}
var playbackState by remember {
mutableStateOf(player.playbackState)
}
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
val scope = rememberCoroutineScope()
DisposableEffect(key1 = player, effect = {
val listener = object : Player.Listener {
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
super.onMediaMetadataChanged(mediaMetadata)
mediaMetadataState = mediaMetadata
duration = 0
position = 0
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
isPlayingState = isPlaying
}
override fun onPlaybackStateChanged(newPlaybackState: Int) {
super.onPlaybackStateChanged(newPlaybackState)
playbackState = newPlaybackState
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
// currentMediaItem = mediaItem
}
}
player.addListener(listener)
onDispose { player.removeListener(listener) }
})
Column(
modifier = modifier,
verticalArrangement = Arrangement.SpaceBetween
) {
IconButton(onClick = { activity?.finish() }) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "")
}
TrackInfo { mediaMetadataState }
PlayerControls(
player = player,
isPlayingProvider = { isPlayingState },
onValueChangeFinished = {
sliderPosition?.let {
position = it
player.seekTo(it)
}
sliderPosition = null
},
onPositionChange = { sliderPosition = it.toLong() },
sliderPosition = { sliderPosition },
position = { position },
duration = { duration },
playbackState = { playbackState }
) {
position = player.currentPosition
duration = player.duration
}
ActionBar(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
isFavoriteProvider = {
true
},
onPlaylistClickedListener = {
scope.launch {
openBottomSheet = true
}
},
onFavoriteClickedListener = {
scope.launch {
// val session = viewModel.fetchSession(player.currentMediaItem?.mediaId)
}
})
}
val skipPartiallyExpanded by remember { mutableStateOf(false) }
val edgeToEdgeEnabled by remember { mutableStateOf(true) }
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = skipPartiallyExpanded
)
if (openBottomSheet) {
val windowInsets = if (edgeToEdgeEnabled)
WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = { openBottomSheet = false },
sheetState = bottomSheetState,
windowInsets = windowInsets
) {
LazyColumn {
items(player.mediaItemCount) {
val mediaItem = player.getMediaItemAt(it)
ListItem(
headlineContent = {
Text("${mediaItem.mediaMetadata.title}")
Text(
text = "${mediaItem.mediaMetadata.description}",
maxLines = 3,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall
)
},
leadingContent = {
AsyncImage(
model = mediaItem.mediaMetadata.artworkUri,
contentDescription = "",
modifier = Modifier.size(64.dp),
)
},
modifier = Modifier.clickable {
player.seekTo(it, 0L)
scope.launch {
bottomSheetState.hide()
}.invokeOnCompletion {
if (!bottomSheetState.isVisible) {
openBottomSheet = false
}
}
}
)
}
}
Spacer(modifier = Modifier.height(48.dp))
}
}
}
}
@Composable
private fun PlayerControls(
modifier: Modifier = Modifier,
player: MediaController,
isPlayingProvider: () -> Boolean,
sliderPosition: () -> Long?,
position: () -> Long,
onValueChangeFinished: () -> Unit,
onPositionChange: (Float) -> Unit,
duration: () -> Long,
playbackState: () -> Int,
onTick: () -> Unit,
) {
Column {
ProgressBar(
modifier = Modifier.padding(16.dp),
position = { (sliderPosition() ?: position()).toFloat() },
duration = { duration().toFloat() },
onPositionChange = onPositionChange,
onValueChangeFinished = onValueChangeFinished,
onTick = onTick,
playbackState = playbackState
)
Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
IconButton(onClick = { player.seekToPreviousMediaItem() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "")
}
IconButton(onClick = { if (isPlayingProvider()) player.pause() else player.play() }) {
Icon(
imageVector = if (isPlayingProvider()) Icons.Filled.ArrowDropDown else Icons.Filled.PlayArrow,
contentDescription = ""
)
}
IconButton(onClick = { player.seekToNextMediaItem() }) {
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "")
}
}
}
}
@Composable
private fun TrackInfo(
modifier: Modifier = Modifier,
mediaMetadata: () -> MediaMetadata?
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
var showShimmer by remember {
mutableStateOf(true)
}
AsyncImage(
model = mediaMetadata()?.artworkUri.toString(), contentDescription = "Album Art",
modifier = Modifier
.size(300.dp)
.clip(RoundedCornerShape(2.dp))
.background(shimmerBrush(showShimmer = showShimmer)),
onSuccess = { showShimmer = false },
contentScale = ContentScale.FillBounds
)
Spacer(modifier = Modifier.height(32.dp))
Text(text = mediaMetadata()?.title.toString(), style = MaterialTheme.typography.titleLarge)
if (mediaMetadata()?.artist != null) {
Spacer(modifier = Modifier.height(16.dp))
Text(text = mediaMetadata()?.artist.toString())
}
}
}
@Composable
private fun ActionBar(
modifier: Modifier = Modifier,
onFavoriteClickedListener: () -> Unit = {},
onShareClickedListener: () -> Unit = {},
onPlaylistClickedListener: () -> Unit = {},
isFavoriteProvider: () -> Boolean,
) {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier.fillMaxWidth()) {
IconButton(onClick = onFavoriteClickedListener) {
Icon(
imageVector = if (isFavoriteProvider()) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
contentDescription = ""
)
}
IconButton(onClick = onShareClickedListener) {
Icon(imageVector = Icons.Outlined.Share, contentDescription = "")
}
IconButton(onClick = { }) {
Icon(imageVector = Icons.Outlined.FavoriteBorder, contentDescription = "")
}
IconButton(onClick = onPlaylistClickedListener) {
Icon(imageVector = Icons.Outlined.FavoriteBorder, contentDescription = "")
}
}
}
@Composable
private fun ProgressBar(
modifier: Modifier = Modifier,
position: () -> Float,
onPositionChange: (Float) -> Unit,
duration: () -> Float,
onValueChangeFinished: () -> Unit,
onTick: () -> Unit,
playbackState: () -> Int
) {
LaunchedEffect(key1 = playbackState(), block = {
if (playbackState() == STATE_READY) {
while (isActive) {
onTick()
delay(500)
}
}
})
Column(modifier = modifier) {
if (duration() < 0) return
Slider(
value = position(),
onValueChange = onPositionChange,
valueRange = 0f..duration(),
onValueChangeFinished = onValueChangeFinished
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
position().toLong().milliseconds.toComponents { hours, minutes, seconds, _ ->
Text(text = "%02d:%02d".format(minutes, seconds))
}
duration().toLong().milliseconds.toComponents { hours, minutes, seconds, _ ->
Text(text = "%02d:%02d".format(minutes, seconds))
}
}
}
}