Untitled

mail@pastecode.io avatar
unknown
kotlin
a year ago
13 kB
7
Indexable
Never
@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))
            }
        }
    }
}