Skip to content

Commit

Permalink
ui: Improved Miniplayer and Fullscreen player
Browse files Browse the repository at this point in the history
refactor: Improved queue logic - Moved from ViewModel to Service

Signed-off-by: Gabriel Fontán <[email protected]>
  • Loading branch information
BobbyESP committed Apr 28, 2024
1 parent 3e60a25 commit e47b95b
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 99 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.bobbyesp.mediaplayer.ext

import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata

fun MediaMetadata.toMediaItem(): MediaItem {
return MediaItem.Builder()
.setMediaMetadata(this)
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ package com.bobbyesp.mediaplayer.service
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY
import androidx.media3.common.Player.EVENT_TIMELINE_CHANGED
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.analytics.PlaybackStats
import androidx.media3.exoplayer.analytics.PlaybackStatsListener
import com.bobbyesp.mediaplayer.ext.toMediaItem
import com.bobbyesp.mediaplayer.service.queue.EmptyQueue
import com.bobbyesp.mediaplayer.service.queue.Queue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject

/**
Expand All @@ -24,16 +34,19 @@ import javax.inject.Inject
class MediaServiceHandler @Inject constructor(
private val player: ExoPlayer
) : Player.Listener, PlaybackStatsListener.Callback {

private val _mediaState = MutableStateFlow<MediaState>(MediaState.Idle)
val mediaState = _mediaState.asStateFlow()

private val _playerActions = MutableStateFlow<PlayerEvent?>(null)
val playerActions = _playerActions.asStateFlow()
private var currentQueue: Queue = EmptyQueue
var queueTitle: String? = null

val currentMediaItem = MutableStateFlow<MediaItem?>(null)

val isThePlayerPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false)

private var job: Job? = null

private val scope = CoroutineScope(Dispatchers.Main)
override fun onIsPlayingChanged(isPlaying: Boolean) {
_mediaState.update {
MediaState.Playing(isPlaying)
Expand Down Expand Up @@ -76,6 +89,58 @@ class MediaServiceHandler @Inject constructor(
player.prepare()
}

fun playQueue(queue: Queue, playWhenReady: Boolean = true) {
currentQueue = queue
queueTitle = null
if (queue.preloadItem != null) {
setMediaItem(queue.preloadItem!!.toMediaItem())
player.playWhenReady = playWhenReady
}

// Launch a new coroutine in the main thread
scope.launch {
// Get the initial state of the queue in the IO thread
// This is a suspending operation and will not block the main thread
val initialState = withContext(Dispatchers.IO) { queue.getInitialData() }

// Set the title of the queue
queueTitle = initialState.title

// Check if the initial state has any items and if the player is not idle or the preload item is null
// If both conditions are met, proceed with the rest of the code
if (initialState.items.isNotEmpty() && !(queue.preloadItem != null && player.playbackState == STATE_IDLE)) {
// If the preload item is not null, add media items to the player
if (queue.preloadItem != null) {
// Add media items from the start of the list to the current media item index
player.addMediaItems(
0,
initialState.items.subList(0, initialState.mediaItemIndex)
)
// Add media items from the current media item index to the end of the list
player.addMediaItems(
initialState.items.subList(
initialState.mediaItemIndex + 1,
initialState.items.size
)
)
} else {
// If the preload item is null, set media items to the player
// If the media item index is greater than 0, use it as the start position
// Otherwise, use 0 as the start position
player.setMediaItems(
initialState.items,
if (initialState.mediaItemIndex > 0) initialState.mediaItemIndex else 0,
initialState.position
)
// Prepare the player for playback
player.prepare()
// Set the player to start playback when it's ready
player.playWhenReady = playWhenReady
}
}
}
}

/**
* Adds a media item to the end of the queue and prepares the player for playback.
* @param mediaItem The media item to be added.
Expand Down Expand Up @@ -156,6 +221,13 @@ class MediaServiceHandler @Inject constructor(
}
}

override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.containsAny(EVENT_TIMELINE_CHANGED, EVENT_POSITION_DISCONTINUITY)) {
currentMediaItem.value = player.currentMediaItem
}
}

fun getActualMediaItem(): MediaItem? {
return player.currentMediaItem
}
Expand Down Expand Up @@ -201,6 +273,14 @@ class MediaServiceHandler @Inject constructor(
}
}

override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (reason == STATE_IDLE) {
currentQueue = EmptyQueue
queueTitle = null
}
}

private suspend fun startProgressUpdate() = job.run {
while (true) {
delay(250)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.bobbyesp.metadator.presentation.components.buttons

import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.rounded.Pause
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp

@Composable
fun PlayPauseAnimatedButton(
modifier: Modifier = Modifier,
isPlaying: Boolean,
onClick: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed = interactionSource.collectIsPressedAsState()
val radius = if (isPlaying || isPressed.value) {
40.dp
} else {
24.dp
}
val cornerRadius = animateDpAsState(targetValue = radius, label = "Animated button shape")

Surface(
tonalElevation = 10.dp,
modifier = modifier
.clip(RoundedCornerShape(cornerRadius.value))
) {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.onSecondary)
.size(72.dp)
.clip(RoundedCornerShape(cornerRadius.value))
.clickable(
interactionSource = interactionSource,
indication = ripple(bounded = false),
) { onClick() },
contentAlignment = Alignment.Center
) {
if (isPlaying) {
Icon(
imageVector = Icons.Rounded.Pause,
contentDescription = "Pause",
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier.size(32.dp)
)
} else {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Play",
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier.size(32.dp)
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import androidx.compose.animation.core.spring
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val CollapsedPlayerHeight = 84.dp
val CollapsedPlayerHeight = 90.dp

val PlayerAnimationSpec = spring<Dp>(stiffness = Spring.StiffnessMediumLow)
Loading

0 comments on commit e47b95b

Please sign in to comment.