Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: SeekBar in listenPlayer #133

Merged
merged 9 commits into from
Jun 2, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,52 @@ import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import com.spotify.protocol.types.PlayerState
import org.listenbrainz.android.R
import org.listenbrainz.android.ui.components.SeekBar
import org.listenbrainz.android.viewmodel.ListensViewModel

@Composable
fun NowPlaying(
playerState: PlayerState?,
bitmap: Bitmap?
bitmap: listenPoster
rishuriya marked this conversation as resolved.
Show resolved Hide resolved
){
val listenViewModel = hiltViewModel<ListensViewModel>()
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clip(RoundedCornerShape(16.dp))
.height(180.dp)
.clickable(onClick = {
//onItemClicked(listen)
val isPaused = playerState?.isPaused ?: false
if (isPaused) {
listenViewModel.play()
} else {
listenViewModel.pause()
}
}),
elevation = 0.dp,
backgroundColor = MaterialTheme.colors.onSurface
) {
Row(
modifier = Modifier
.padding(16.dp)
.padding(10.dp)
) {
Text(
text = "Now playing",
Expand All @@ -53,15 +65,16 @@ fun NowPlaying(
textAlign = TextAlign.Center,
)
}
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.padding(top = 40.dp)
.padding(top = 30.dp)
) {
val painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current)
.data(data = bitmap)
.data(data = bitmap.bitmap)
.placeholder(R.drawable.ic_coverartarchive_logo_no_text)
.error(R.drawable.ic_coverartarchive_logo_no_text)
.build()
Expand All @@ -83,31 +96,30 @@ fun NowPlaying(
playerState?.track?.name?.let { track ->
Text(
text = track,

modifier = Modifier.padding(0.dp, 0.dp, 12.dp, 0.dp),
modifier = Modifier.padding(0.dp, 0.dp, 5.dp, 0.dp),
color = MaterialTheme.colors.surface,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.subtitle1,
maxLines = 1
)
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(6.dp))

Text(
text = buildString {
append(playerState?.track?.artist?.name)
},
modifier = Modifier.padding(0.dp, 0.dp, 12.dp, 0.dp),
modifier = Modifier.padding(0.dp, 0.dp, 5.dp, 0.dp),
color = MaterialTheme.colors.surface,
style = MaterialTheme.typography.caption,
maxLines = 2
)

Row(verticalAlignment = Alignment.Bottom) {
Row {
playerState?.track?.album?.name?.let { album ->
Text(
text = album,
modifier = Modifier.padding(0.dp, 12.dp, 12.dp, 0.dp),
modifier = Modifier.padding(0.dp, 6.dp, 12.dp, 0.dp),
color = MaterialTheme.colors.surface,
style = MaterialTheme.typography.caption,
maxLines = 2
Expand All @@ -116,6 +128,16 @@ fun NowPlaying(
}
}
}
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.Bottom,
) {
ProgressBar(playerState = playerState)
}
}

}
}

Expand All @@ -124,6 +146,78 @@ fun NowPlaying(
fun NowPlayingPreview() {
NowPlaying(
playerState = null,
bitmap = null
bitmap = listenPoster()
)
}
}
@Composable
fun ProgressBar(playerState: PlayerState?) {
rishuriya marked this conversation as resolved.
Show resolved Hide resolved
val listenViewModel = hiltViewModel<ListensViewModel>()
val progress by listenViewModel.progress.collectAsState(initial = 0f)
Column {
Box {
SeekBar(
modifier = Modifier
.height(10.dp)
.fillMaxWidth(0.98F)
.padding(0.dp),
progress = progress,
onValueChange = {//get the value of the seekbar
listenViewModel.seekTo(it, playerState)
},
onValueChanged = { }
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth(0.98F)
.padding(start = 10.dp, top = 0.dp, end = 10.dp)
) {
val song = playerState?.track
var duration = "00:00"
val songCurrentPosition by listenViewModel.songCurrentPosition.collectAsState()
var currentPosition = "00:00"
if ((song?.duration ?: 0) / (1000 * 60 * 60) > 0 && songCurrentPosition / (1000 * 60 * 60) > 0) {
duration = String.format(
"%02d:%02d:%02d",
(song?.duration ?: 0) / (1000 * 60 * 60),
(song?.duration ?: 0) / (1000 * 60) % 60,
(song?.duration ?: 0) / 1000 % 60
)
currentPosition = String.format(
"%02d:%02d:%02d",
songCurrentPosition / (1000 * 60 * 60),
songCurrentPosition / (1000 * 60) % 60,
songCurrentPosition / 1000 % 60
)
} else {
duration = String.format(
"%02d:%02d",
(song?.duration ?: 0) / (1000 * 60) % 60,
(song?.duration ?: 0) / 1000 % 60
)
currentPosition =
String.format("%02d:%02d", songCurrentPosition / (1000 * 60) % 60, songCurrentPosition / 1000 % 60)
}
Text(
text = currentPosition,
textAlign = TextAlign.Start,
color = Color.White,
modifier = Modifier.padding(end = 5.dp)
)

Text(
text = duration,
textAlign = TextAlign.Start,
color = Color.White,
modifier = Modifier.padding(start = 5.dp)
)
}
}
}

data class listenPoster(
rishuriya marked this conversation as resolved.
Show resolved Hide resolved
val bitmap: Bitmap?=null,
val id:String?=""
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.listenbrainz.android.viewmodel

import android.app.Application
import android.graphics.Bitmap
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -19,6 +18,8 @@ import com.spotify.protocol.client.Subscription
import com.spotify.protocol.types.PlayerContext
import com.spotify.protocol.types.PlayerState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
Expand All @@ -27,10 +28,10 @@ import org.listenbrainz.android.BuildConfig
import org.listenbrainz.android.model.Listen
import org.listenbrainz.android.repository.ListensRepository
import org.listenbrainz.android.service.YouTubeApiService
import org.listenbrainz.android.ui.screens.listens.listenPoster
import org.listenbrainz.android.util.Constants
import org.listenbrainz.android.util.Log.d
import org.listenbrainz.android.util.Log.e
import org.listenbrainz.android.util.Log.v
import org.listenbrainz.android.util.Resource.Status.*
import org.listenbrainz.android.util.Utils.getCoverArtUrl
import retrofit2.Retrofit
Expand All @@ -56,10 +57,14 @@ class ListensViewModel @Inject constructor(
val coverArtFlow = _coverArtFlow.asStateFlow()

var isLoading: Boolean by mutableStateOf(true)

var playerState: PlayerState? by mutableStateOf(null)
var bitmap: Bitmap? by mutableStateOf(null)

private val _songDuration = MutableStateFlow(0L)
private val _songCurrentPosition = MutableStateFlow(0L)
private val _progress = MutableStateFlow(0F)
var bitmap: listenPoster = listenPoster()
val progress = _progress.asStateFlow()
val songCurrentPosition = _songCurrentPosition.asStateFlow()
private val gson = GsonBuilder().setPrettyPrinting().create()

private var playerStateSubscription: Subscription<PlayerState>? = null
Expand All @@ -70,8 +75,9 @@ class ListensViewModel @Inject constructor(

init {
SpotifyAppRemote.setDebugMode(BuildConfig.DEBUG)
trackProgress()
}

fun fetchUserListens(userName: String) {
viewModelScope.launch {
val response = repository.fetchUserListens(userName)
Expand Down Expand Up @@ -141,7 +147,10 @@ class ListensViewModel @Inject constructor(
private fun updateTrackCoverArt(playerState: PlayerState) {
// Get image from track
assertAppRemoteConnected()?.imagesApi?.getImage(playerState.track.imageUri, com.spotify.protocol.types.Image.Dimension.LARGE)?.setResultCallback { bitmapHere ->
bitmap = bitmapHere
bitmap =listenPoster(
bitmap=bitmapHere,
id = playerState.track.uri
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have just made a separate variable here. Why store it with the bitmap like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, bitmap is updated from event callback, which is called every second to get song position and to avoid updating of bitmap again and again, the function will only run when uri which it store is different from the current song.

)
}
}

Expand All @@ -167,11 +176,7 @@ class ListensViewModel @Inject constructor(
}

private val playerStateEventCallback = Subscription.EventCallback<PlayerState> { playerStateHere ->
v(String.format("Player State: %s", gson.toJson(playerStateHere)))

playerState = playerStateHere

updateTrackCoverArt(playerStateHere)
}

private suspend fun connectToAppRemote(showAuthView: Boolean, spotifyClientId: String): SpotifyAppRemote =
Expand Down Expand Up @@ -211,8 +216,58 @@ class ListensViewModel @Inject constructor(
assertAppRemoteConnected()?.playerApi?.play(uri)?.setResultCallback {
logMessage("play command successful!") //getString(R.string.command_feedback, "play"))
}?.setErrorCallback(errorCallback)
trackProgress()
}

fun play(){
assertAppRemoteConnected()?.playerApi?.resume()?.setResultCallback {
logMessage("play command successful!") //getString(R.string.command_feedback, "play"))
}?.setErrorCallback(errorCallback)
trackProgress()
}

fun pause(){
assertAppRemoteConnected()?.playerApi?.pause()?.setResultCallback {
logMessage("pause command successful!") //getString(R.string.command_feedback, "play"))
}?.setErrorCallback(errorCallback)
}
fun trackProgress() {
assertAppRemoteConnected()?.playerApi?.subscribeToPlayerState()?.setEventCallback { playerState ->
if(bitmap.id!=playerState.track.uri) {
updateTrackCoverArt(playerState)
}
}?.setErrorCallback(errorCallback)
viewModelScope.launch(Dispatchers.Default) {
var state: PlayerState? = null
var isPaused=false
while (true) {
assertAppRemoteConnected()?.playerApi?.subscribeToPlayerState()?.setEventCallback { playerState ->
state = playerState
isPaused = playerState?.isPaused ?: false

}?.setErrorCallback(errorCallback)
val pos = state?.playbackPosition?.toFloat() ?: 0f
val duration=state?.track?.duration ?: 1
if (progress.value != pos) {
_progress.emit(pos / duration.toFloat())
_songDuration.emit(duration ?: 0)
_songCurrentPosition.emit(((pos / duration) * duration).toLong())
}
if (isPaused) {
break
}
delay(1000L)
}
}
}

fun seekTo(pos:Float,state: PlayerState?){
val duration=state?.track?.duration ?: 1
val position=(pos*duration).toLong()
assertAppRemoteConnected()?.playerApi?.seekTo(position)?.setResultCallback {
logMessage("seek command successful!") //getString(R.string.command_feedback, "play"))
}?.setErrorCallback(errorCallback)
}

private fun onSubscribedToPlayerContextButtonClicked() {
playerContextSubscription = cancelAndResetSubscription(playerContextSubscription)
playerContextSubscription = assertAppRemoteConnected()?.playerApi?.subscribeToPlayerContext()?.setEventCallback(playerContextEventCallback)?.setErrorCallback { throwable ->
Expand Down