Add landscape mode to PlayerView

This commit is contained in:
vfsfitvnm 2022-07-12 10:54:21 +02:00
parent 18362e99b0
commit c55bccc7aa

View file

@ -1,6 +1,7 @@
package it.vfsfitvnm.vimusic.ui.views package it.vfsfitvnm.vimusic.ui.views
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.media.audiofx.AudioEffect import android.media.audiofx.AudioEffect
import android.text.format.DateUtils import android.text.format.DateUtils
import android.text.format.Formatter import android.text.format.Formatter
@ -12,12 +13,10 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -30,6 +29,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -50,11 +50,8 @@ import it.vfsfitvnm.vimusic.ui.components.themed.BaseMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
import it.vfsfitvnm.vimusic.ui.styling.* import it.vfsfitvnm.vimusic.ui.styling.*
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.PlayerResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -65,11 +62,12 @@ fun PlayerView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
val preferences = LocalPreferences.current
val colorPalette = LocalColorPalette.current val colorPalette = LocalColorPalette.current
val typography = LocalTypography.current val typography = LocalTypography.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val context = LocalContext.current val context = LocalContext.current
val configuration = LocalConfiguration.current
val player = binder?.player val player = binder?.player
val playerState = rememberPlayerState(player) val playerState = rememberPlayerState(player)
@ -77,11 +75,6 @@ fun PlayerView(
player ?: return player ?: return
playerState?.mediaItem ?: return playerState?.mediaItem ?: return
val coroutineScope = rememberCoroutineScope()
val (thumbnailSizeDp, thumbnailSizePx) = Dimensions.thumbnails.player.song.let {
it to (it - 64.dp).px
}
BottomSheet( BottomSheet(
state = layoutState, state = layoutState,
@ -177,19 +170,70 @@ fun PlayerView(
playerState.mediaItem.mediaId.let(Database::song).distinctUntilChanged() playerState.mediaItem.mediaId.let(Database::song).distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO) }.collectAsState(initial = null, context = Dispatchers.IO)
var isShowingStatsForNerds by rememberSaveable { when (configuration.orientation) {
mutableStateOf(false) Configuration.ORIENTATION_LANDSCAPE -> {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(bottom = 64.dp)
.background(colorPalette.background)
.padding(top = 16.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.weight(0.66f)
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
Thumbnail(
playerState = playerState,
song = song,
modifier = Modifier
)
} }
Controls(
playerState = playerState,
song = song,
modifier = Modifier
.padding(vertical = 8.dp)
.fillMaxHeight()
.weight(1f)
)
}
}
else -> {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.padding(bottom = 64.dp)
.background(colorPalette.background) .background(colorPalette.background)
.padding(bottom = 72.dp) .padding(top = 16.dp)
.fillMaxSize()
) { ) {
var scrubbingPosition by remember(playerState.mediaItemIndex) { Box(
mutableStateOf<Long?>(null) contentAlignment = Alignment.Center,
modifier = Modifier
.weight(1.25f)
.padding(horizontal = 32.dp, vertical = 8.dp)
) {
Thumbnail(
playerState = playerState,
song = song,
modifier = Modifier
)
}
Controls(
playerState = playerState,
song = song,
modifier = Modifier
.padding(vertical = 8.dp)
.fillMaxWidth()
.weight(1f)
)
}
}
} }
TopAppBar { TopAppBar {
@ -211,18 +255,37 @@ fun PlayerView(
BaseMediaItemMenu( BaseMediaItemMenu(
mediaItem = playerState.mediaItem, mediaItem = playerState.mediaItem,
onGoToEqualizer = { onGoToEqualizer = {
val intent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { val intent =
putExtra(AudioEffect.EXTRA_AUDIO_SESSION, player.audioSessionId) Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) putExtra(
putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) AudioEffect.EXTRA_AUDIO_SESSION,
player.audioSessionId
)
putExtra(
AudioEffect.EXTRA_PACKAGE_NAME,
context.packageName
)
putExtra(
AudioEffect.EXTRA_CONTENT_TYPE,
AudioEffect.CONTENT_TYPE_MUSIC
)
} }
if (intent.resolveActivity(context.packageManager) != null) { if (intent.resolveActivity(context.packageManager) != null) {
val contract = ActivityResultContracts.StartActivityForResult() val contract =
ActivityResultContracts.StartActivityForResult()
resultRegistryOwner?.activityResultRegistry?.register("", contract) {}?.launch(intent) resultRegistryOwner?.activityResultRegistry
?.register("", contract) {}
?.launch(intent)
} else { } else {
Toast.makeText(context, "No equalizer app found!", Toast.LENGTH_SHORT).show() Toast
.makeText(
context,
"No equalizer app found!",
Toast.LENGTH_SHORT
)
.show()
} }
}, },
onDismiss = menuState::hide, onDismiss = menuState::hide,
@ -235,6 +298,41 @@ fun PlayerView(
) )
} }
PlayerBottomSheet(
playerState = playerState,
layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound * 0.9f),
onGlobalRouteEmitted = layoutState.collapse,
padding = layoutState.upperBound * 0.1f,
song = song,
modifier = Modifier
.align(Alignment.BottomCenter)
)
}
}
@ExperimentalAnimationApi
@Composable
private fun Thumbnail(
playerState: PlayerState,
song: Song?,
modifier: Modifier = Modifier
) {
val typography = LocalTypography.current
val context = LocalContext.current
val binder = LocalPlayerServiceBinder.current
val player = binder?.player ?: return
playerState.mediaItem ?: return
val (thumbnailSizeDp, thumbnailSizePx) = Dimensions.thumbnails.player.song.let {
it to (it - 64.dp).px
}
var isShowingStatsForNerds by rememberSaveable {
mutableStateOf(false)
}
if (playerState.error == null) { if (playerState.error == null) {
AnimatedContent( AnimatedContent(
targetState = playerState.mediaItemIndex, targetState = playerState.mediaItemIndex,
@ -247,9 +345,8 @@ fun PlayerView(
SizeTransform(clip = false) SizeTransform(clip = false)
) )
}, },
modifier = Modifier modifier = modifier
.weight(1f) .aspectRatio(1f)
.align(Alignment.CenterHorizontally)
) { ) {
val artworkUri = remember(it) { val artworkUri = remember(it) {
player.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail( player.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail(
@ -259,9 +356,6 @@ fun PlayerView(
Box( Box(
modifier = Modifier modifier = Modifier
.padding(bottom = 32.dp)
.padding(horizontal = 32.dp)
.aspectRatio(1f)
.clip(ThumbnailRoundness.shape) .clip(ThumbnailRoundness.shape)
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
) { ) {
@ -280,7 +374,7 @@ fun PlayerView(
.fillMaxSize() .fillMaxSize()
) )
androidx.compose.animation.AnimatedVisibility( AnimatedVisibility(
visible = isShowingStatsForNerds, visible = isShowingStatsForNerds,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut(), exit = fadeOut(),
@ -404,40 +498,6 @@ fun PlayerView(
) )
} }
} }
if (song != null && (contentLength == null || loudnessDb == null)) {
BasicText(
text = "FILL MISSING DATA",
style = typography.xxs.semiBold.color(BlackColorPalette.text),
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = {
song?.let { song ->
coroutineScope.launch(Dispatchers.IO) {
YouTube
.player(song.id)
?.map { body ->
Database.update(
song.copy(
loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(),
contentLength = body.streamingData?.adaptiveFormats
?.findLast { format ->
format.itag == 251
}
?.let(PlayerResponse.StreamingData.AdaptiveFormat::contentLength)
)
)
}
}
}
}
)
.padding(all = 16.dp)
.align(Alignment.End)
)
}
} }
} }
} }
@ -445,9 +505,7 @@ fun PlayerView(
} else { } else {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier modifier = modifier
.weight(1f)
.align(Alignment.CenterHorizontally)
.padding(bottom = 32.dp) .padding(bottom = 32.dp)
.padding(horizontal = 32.dp) .padding(horizontal = 32.dp)
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
@ -461,23 +519,53 @@ fun PlayerView(
) {} ) {}
} }
} }
}
@Composable
private fun Controls(
playerState: PlayerState,
song: Song?,
modifier: Modifier = Modifier
) {
val typography = LocalTypography.current
val colorPalette = LocalColorPalette.current
val preferences = LocalPreferences.current
val binder = LocalPlayerServiceBinder.current
val player = binder?.player ?: return
var scrubbingPosition by remember(playerState.mediaItemIndex) {
mutableStateOf<Long?>(null)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 32.dp)
) {
Spacer(
modifier = Modifier
.weight(1f)
)
BasicText( BasicText(
text = playerState.mediaMetadata.title?.toString() ?: "", text = playerState.mediaMetadata.title?.toString() ?: "",
style = typography.l.bold, style = typography.l.bold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
modifier = Modifier
.padding(horizontal = 32.dp)
) )
BasicText( BasicText(
text = playerState.mediaMetadata.artist?.toString() ?: "", text = playerState.mediaMetadata.artist?.toString() ?: "",
style = typography.s.semiBold.secondary, style = typography.s.semiBold.secondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
)
Spacer(
modifier = Modifier modifier = Modifier
.padding(horizontal = 32.dp) .weight(0.5f)
) )
SeekBar( SeekBar(
@ -500,20 +588,19 @@ fun PlayerView(
}, },
color = colorPalette.text, color = colorPalette.text,
backgroundColor = colorPalette.textDisabled, backgroundColor = colorPalette.textDisabled,
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp)
)
Spacer(
modifier = Modifier modifier = Modifier
.padding(top = 24.dp, bottom = 12.dp) .height(8.dp)
.padding(horizontal = 32.dp)
.fillMaxWidth()
) )
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.padding(horizontal = 32.dp)
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp)
) { ) {
BasicText( BasicText(
text = DateUtils.formatElapsedTime( text = DateUtils.formatElapsedTime(
@ -534,11 +621,16 @@ fun PlayerView(
} }
} }
Spacer(
modifier = Modifier
.weight(1f)
)
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.padding(vertical = 32.dp) .fillMaxWidth()
) { ) {
Image( Image(
painter = painterResource(R.drawable.heart), painter = painterResource(R.drawable.heart),
@ -551,10 +643,10 @@ fun PlayerView(
query { query {
song?.let { song -> song?.let { song ->
Database.update(song.toggleLike()) Database.update(song.toggleLike())
} ?: Database.insert(playerState.mediaItem, Song::toggleLike) } ?: Database.insert(playerState.mediaItem!!, Song::toggleLike)
} }
} }
.padding(horizontal = 16.dp) .weight(1f)
.size(28.dp) .size(28.dp)
) )
@ -564,15 +656,20 @@ fun PlayerView(
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable(onClick = player::seekToPrevious) .clickable(onClick = player::seekToPrevious)
.padding(horizontal = 16.dp) .weight(1f)
.size(28.dp) .size(28.dp)
) )
val isPaused = playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady Spacer(
modifier = Modifier
.width(8.dp)
)
val isPaused =
playerState.playbackState == Player.STATE_ENDED || !playerState.playWhenReady
Box( Box(
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp)
.clickable { .clickable {
if (isPaused) { if (isPaused) {
if (player.playbackState == Player.STATE_IDLE) { if (player.playbackState == Player.STATE_IDLE) {
@ -597,13 +694,18 @@ fun PlayerView(
) )
} }
Spacer(
modifier = Modifier
.width(8.dp)
)
Image( Image(
painter = painterResource(R.drawable.play_skip_forward), painter = painterResource(R.drawable.play_skip_forward),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text), colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable(onClick = player::seekToNext) .clickable(onClick = player::seekToNext)
.padding(horizontal = 16.dp) .weight(1f)
.size(28.dp) .size(28.dp)
) )
@ -633,21 +735,14 @@ fun PlayerView(
preferences.repeatMode = repeatMode preferences.repeatMode = repeatMode
} }
} }
.padding(horizontal = 16.dp) .weight(1f)
.size(28.dp) .size(28.dp)
) )
} }
}
PlayerBottomSheet( Spacer(
playerState = playerState,
layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound * 0.9f),
onGlobalRouteEmitted = layoutState.collapse,
padding = layoutState.upperBound * 0.1f,
song = song,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .weight(1f)
) )
} }
} }