From b73736bd5a3e5cb7d212c5fc4c4691dd476ef45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Thu, 18 Apr 2024 17:29:45 +0200 Subject: [PATCH] feat/WIP: Started creating some UI and fixed playing state not being well updated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gabriel Fontán --- .../service/MediaServiceHandler.kt | 34 ++-- .../mediaplayer/service/queue/EmptyQueue.kt | 13 ++ .../mediaplayer/service/queue/Queue.kt | 22 +++ .../mediaplayer/service/queue/SongsQueue.kt | 18 ++ .../components/others/MediaplayerSheet.kt | 179 +++++++++++++++--- .../pages/mediaplayer/MediaplayerViewModel.kt | 24 +-- 6 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/EmptyQueue.kt create mode 100644 app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/Queue.kt create mode 100644 app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/SongsQueue.kt diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaServiceHandler.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaServiceHandler.kt index d639394..e30398c 100644 --- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaServiceHandler.kt +++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaServiceHandler.kt @@ -22,8 +22,20 @@ class MediaServiceHandler @Inject constructor( private val _mediaState = MutableStateFlow(MediaState.Idle) val mediaState = _mediaState.asStateFlow() + val isThePlayerPlaying: MutableStateFlow = MutableStateFlow(false) + private var job: Job? = null + override fun onIsPlayingChanged(isPlaying: Boolean) { + _mediaState.update { + MediaState.Playing(isPlaying) + } + isThePlayerPlaying.update { + isPlaying + } + super.onIsPlayingChanged(isPlaying) + } + init { player.addListener(this) job = Job() @@ -107,22 +119,12 @@ class MediaServiceHandler @Inject constructor( suspend fun onPlayerEvent(playerEvent: PlayerEvent) { when (playerEvent) { is PlayerEvent.PlayPause -> { - when (player.isPlaying) { - true -> { - player.pause() - _mediaState.update { - MediaState.Playing(false) - } - stopProgressUpdate() - } - - false -> { - player.play() - _mediaState.update { - MediaState.Playing(true) - } - startProgressUpdate() - } + if (player.isPlaying) { + player.pause() + stopProgressUpdate() + } else { + player.play() + startProgressUpdate() } } diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/EmptyQueue.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/EmptyQueue.kt new file mode 100644 index 0000000..cdf97db --- /dev/null +++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/EmptyQueue.kt @@ -0,0 +1,13 @@ +package com.bobbyesp.mediaplayer.service.queue + +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata + +object EmptyQueue : Queue { + override val preloadItem: MediaMetadata? + get() = null + + override suspend fun getInitialData(): Queue.Data = Queue.Data.empty() + override fun hasNextPage(): Boolean = false + override suspend fun nextPage(): List = emptyList() +} \ No newline at end of file diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/Queue.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/Queue.kt new file mode 100644 index 0000000..138a47d --- /dev/null +++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/Queue.kt @@ -0,0 +1,22 @@ +package com.bobbyesp.mediaplayer.service.queue + +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata + +interface Queue { + val preloadItem: MediaMetadata? + suspend fun getInitialData(): Data + fun hasNextPage(): Boolean + suspend fun nextPage(): List + + data class Data( + val title: String?, + val items: List, + val mediaItemIndex: Int, + val position: Long = 0L, + ) { + companion object { + fun empty() = Data(null, emptyList(), -1, 0L) + } + } +} \ No newline at end of file diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/SongsQueue.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/SongsQueue.kt new file mode 100644 index 0000000..2a5894f --- /dev/null +++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/SongsQueue.kt @@ -0,0 +1,18 @@ +package com.bobbyesp.mediaplayer.service.queue + +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata + +data class SongsQueue( + val title: String? = null, + val items: List, + val startIndex: Int = 0, + val position: Long = 0L, +) : Queue { + override val preloadItem: MediaMetadata? = null + override suspend fun getInitialData(): Queue.Data = + Queue.Data(title, items, startIndex, position) + + override fun hasNextPage(): Boolean = false + override suspend fun nextPage() = throw UnsupportedOperationException() +} diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt index 8e3931b..3b1cfba 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt @@ -1,6 +1,7 @@ package com.bobbyesp.metadator.presentation.components.others -import androidx.compose.foundation.ExperimentalFoundationApi +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -8,12 +9,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Pause @@ -27,14 +30,18 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.media3.common.MediaMetadata import com.bobbyesp.metadator.R import com.bobbyesp.metadator.presentation.components.image.ArtworkAsyncImage import com.bobbyesp.metadator.presentation.pages.mediaplayer.MediaplayerViewModel +import com.bobbyesp.metadator.presentation.theme.MetadatorTheme import com.bobbyesp.model.Song import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheet import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheetState @@ -57,23 +64,26 @@ fun MediaplayerSheet( }, backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(NavigationBarDefaults.Elevation) ) { - + MediaplayerExpandedContent( + viewModel = viewModel, + sheetState = state + ) } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun MediaplayerCollapsedContent( modifier: Modifier = Modifier, viewModel: MediaplayerViewModel, ) { val viewState = viewModel.pageViewState.collectAsStateWithLifecycle().value - val playingSong = viewState.playingSong ?: return - val playerState = viewState.uiState + val playingSong = viewState.playingSong ?: return + val progress = (playerState as? MediaplayerViewModel.PlayerState.Ready)?.progress ?: 0f - val isPlaying = (playerState as? MediaplayerViewModel.PlayerState.Ready)?.isPlaying ?: false + val isPlaying = viewModel.isPlaying.collectAsStateWithLifecycle().value + Box( modifier = modifier .fillMaxWidth() @@ -91,6 +101,108 @@ private fun MediaplayerCollapsedContent( } } +@Composable +private fun MediaplayerExpandedContent( + modifier: Modifier = Modifier, + viewModel: MediaplayerViewModel, + sheetState: DraggableBottomSheetState +) { + val viewState = viewModel.pageViewState.collectAsStateWithLifecycle().value + val playerState = viewState.uiState + + val playingSong = viewState.queueSongs.items.getOrNull(0)?.mediaMetadata ?: return + + val progress = (playerState as? MediaplayerViewModel.PlayerState.Ready)?.progress ?: 0f + val isPlaying = viewModel.isPlaying.collectAsStateWithLifecycle().value + + Box( + modifier = modifier + .fillMaxSize() + .systemBarsPadding(), + ) { + when (LocalConfiguration.current.orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + Row( + modifier = Modifier + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)) + .padding(bottom = sheetState.collapsedBound) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.weight(1f) + ) { + SongInformation( + mediaMetadata = playingSong, + modifier = Modifier.padding(8.dp) + ) + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .weight(1f) + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)) + ) { + + } + } + } + + else -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = sheetState.collapsedBound) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + } + } + } + } + } +} + +@Composable +private fun SongInformation( + modifier: Modifier = Modifier, + mediaMetadata: MediaMetadata +) { + val config = LocalConfiguration.current + val screenHeight = config.screenHeightDp.dp + val screenWidth = config.screenWidthDp.dp + + Column( + modifier = modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + ArtworkAsyncImage( + modifier = Modifier + .size(screenWidth / 2) + .clip(MaterialTheme.shapes.medium), + artworkPath = mediaMetadata.artworkUri + ) + MarqueeText( + text = mediaMetadata.title.toString(), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + MarqueeText( + text = mediaMetadata.artist.toString(), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ), + fontSize = 12.sp + ) + } +} + @Composable fun SongCard( modifier: Modifier = Modifier, @@ -173,23 +285,38 @@ fun SongCard( } } -//@Preview -//@Preview(uiMode = UI_MODE_NIGHT_YES) -//@Composable -//private fun CollapsedContentPrev() { -// MetadatorTheme { -// MediaplayerCollapsedContent( -// queueSongs = listOf( -// Song( -// id = 1, -// title = "Bones", -// artist = "Imagine Dragons", -// album = "Mercury - Acts 1 & 2", -// artworkPath = null, -// duration = 100.0, -// path = "path" -// ) -// ) -// ) -// } -//} \ No newline at end of file + +@Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun SongInformationPrev() { + MetadatorTheme { + SongInformation( + mediaMetadata = MediaMetadata.Builder() + .setTitle("Bones") + .setArtist("Imagine Dragons") + .setAlbumTitle("Mercury - Acts 1 & 2") + .setArtworkUri(null) + .build() + ) + } +} + +@Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun CollapsedContentPrev() { + MetadatorTheme { + SongCard( + playingSong = Song( + id = 1, + title = "Bones", + artist = "Imagine Dragons", + album = "Mercury - Acts 1 & 2", + artworkPath = null, + duration = 100.0, + path = "path" + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerViewModel.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerViewModel.kt index 59beb1b..8e47f1d 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerViewModel.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerViewModel.kt @@ -8,17 +8,16 @@ import androidx.media3.common.MediaMetadata import com.bobbyesp.mediaplayer.service.MediaServiceHandler import com.bobbyesp.mediaplayer.service.MediaState import com.bobbyesp.mediaplayer.service.PlayerEvent +import com.bobbyesp.mediaplayer.service.queue.SongsQueue import com.bobbyesp.model.Song import com.bobbyesp.utilities.Time.formatDuration import com.bobbyesp.utilities.mediastore.MediaStoreReceiver.Advanced.observeSongs import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -33,15 +32,17 @@ class MediaplayerViewModel @Inject constructor( val songsFlow = applicationContext.contentResolver.observeSongs() + val isPlaying = serviceHandler.isThePlayerPlaying + data class MediaplayerPageState( val uiState: PlayerState = PlayerState.Initial, val playingSong: Song? = null, - val queueSongs: Flow> = emptyFlow() + val queueSongs: SongsQueue = SongsQueue(items = emptyList()) ) init { viewModelScope.launch(Dispatchers.IO) { - serviceHandler.mediaState.collect { mediaState -> + serviceHandler.mediaState.collectLatest { mediaState -> when (mediaState) { is MediaState.Buffering -> calculateProgressValues(mediaState.progress) is MediaState.Playing -> mutableMediaplayerPageState.update { @@ -81,12 +82,6 @@ class MediaplayerViewModel @Inject constructor( } private fun loadSongInfo(song: Song) { - mutableMediaplayerPageState.update { - it.copy( - playingSong = song, - queueSongs = flowOf(listOf(song)) - ) - } val mediaItem = MediaItem.Builder() .setUri(song.path) .setMediaMetadata( @@ -98,6 +93,13 @@ class MediaplayerViewModel @Inject constructor( .build() ).build() + mutableMediaplayerPageState.update { + it.copy( + playingSong = song, + queueSongs = SongsQueue(items = listOf(mediaItem)) + ) + } + viewModelScope.launch { serviceHandler.setMediaItem(mediaItem) }