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
import android.content.Intent
import android.content.res.Configuration
import android.media.audiofx.AudioEffect
import android.text.format.DateUtils
import android.text.format.Formatter
@ -12,12 +13,10 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
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.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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.styling.*
import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.PlayerResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@ -65,11 +62,12 @@ fun PlayerView(
modifier: Modifier = Modifier,
) {
val menuState = LocalMenuState.current
val preferences = LocalPreferences.current
val colorPalette = LocalColorPalette.current
val typography = LocalTypography.current
val binder = LocalPlayerServiceBinder.current
val context = LocalContext.current
val configuration = LocalConfiguration.current
val player = binder?.player
val playerState = rememberPlayerState(player)
@ -77,11 +75,6 @@ fun PlayerView(
player ?: return
playerState?.mediaItem ?: return
val coroutineScope = rememberCoroutineScope()
val (thumbnailSizeDp, thumbnailSizePx) = Dimensions.thumbnails.player.song.let {
it to (it - 64.dp).px
}
BottomSheet(
state = layoutState,
@ -177,19 +170,70 @@ fun PlayerView(
playerState.mediaItem.mediaId.let(Database::song).distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO)
var isShowingStatsForNerds by rememberSaveable {
mutableStateOf(false)
when (configuration.orientation) {
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(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(bottom = 64.dp)
.background(colorPalette.background)
.padding(bottom = 72.dp)
.fillMaxSize()
.padding(top = 16.dp)
) {
var scrubbingPosition by remember(playerState.mediaItemIndex) {
mutableStateOf<Long?>(null)
Box(
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 {
@ -211,18 +255,37 @@ fun PlayerView(
BaseMediaItemMenu(
mediaItem = playerState.mediaItem,
onGoToEqualizer = {
val intent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra(AudioEffect.EXTRA_AUDIO_SESSION, player.audioSessionId)
putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
val intent =
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra(
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) {
val contract = ActivityResultContracts.StartActivityForResult()
val contract =
ActivityResultContracts.StartActivityForResult()
resultRegistryOwner?.activityResultRegistry?.register("", contract) {}?.launch(intent)
resultRegistryOwner?.activityResultRegistry
?.register("", contract) {}
?.launch(intent)
} 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,
@ -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) {
AnimatedContent(
targetState = playerState.mediaItemIndex,
@ -247,9 +345,8 @@ fun PlayerView(
SizeTransform(clip = false)
)
},
modifier = Modifier
.weight(1f)
.align(Alignment.CenterHorizontally)
modifier = modifier
.aspectRatio(1f)
) {
val artworkUri = remember(it) {
player.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail(
@ -259,9 +356,6 @@ fun PlayerView(
Box(
modifier = Modifier
.padding(bottom = 32.dp)
.padding(horizontal = 32.dp)
.aspectRatio(1f)
.clip(ThumbnailRoundness.shape)
.size(thumbnailSizeDp)
) {
@ -280,7 +374,7 @@ fun PlayerView(
.fillMaxSize()
)
androidx.compose.animation.AnimatedVisibility(
AnimatedVisibility(
visible = isShowingStatsForNerds,
enter = fadeIn(),
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 {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.weight(1f)
.align(Alignment.CenterHorizontally)
modifier = modifier
.padding(bottom = 32.dp)
.padding(horizontal = 32.dp)
.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(
text = playerState.mediaMetadata.title?.toString() ?: "",
style = typography.l.bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(horizontal = 32.dp)
overflow = TextOverflow.Ellipsis
)
BasicText(
text = playerState.mediaMetadata.artist?.toString() ?: "",
style = typography.s.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis
)
Spacer(
modifier = Modifier
.padding(horizontal = 32.dp)
.weight(0.5f)
)
SeekBar(
@ -500,20 +588,19 @@ fun PlayerView(
},
color = colorPalette.text,
backgroundColor = colorPalette.textDisabled,
shape = RoundedCornerShape(8.dp),
shape = RoundedCornerShape(8.dp)
)
Spacer(
modifier = Modifier
.padding(top = 24.dp, bottom = 12.dp)
.padding(horizontal = 32.dp)
.fillMaxWidth()
.height(8.dp)
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 32.dp)
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
BasicText(
text = DateUtils.formatElapsedTime(
@ -534,11 +621,16 @@ fun PlayerView(
}
}
Spacer(
modifier = Modifier
.weight(1f)
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = 32.dp)
.fillMaxWidth()
) {
Image(
painter = painterResource(R.drawable.heart),
@ -551,10 +643,10 @@ fun PlayerView(
query {
song?.let { song ->
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)
)
@ -564,15 +656,20 @@ fun PlayerView(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = player::seekToPrevious)
.padding(horizontal = 16.dp)
.weight(1f)
.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(
modifier = Modifier
.padding(horizontal = 8.dp)
.clickable {
if (isPaused) {
if (player.playbackState == Player.STATE_IDLE) {
@ -597,13 +694,18 @@ fun PlayerView(
)
}
Spacer(
modifier = Modifier
.width(8.dp)
)
Image(
painter = painterResource(R.drawable.play_skip_forward),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = player::seekToNext)
.padding(horizontal = 16.dp)
.weight(1f)
.size(28.dp)
)
@ -633,21 +735,14 @@ fun PlayerView(
preferences.repeatMode = repeatMode
}
}
.padding(horizontal = 16.dp)
.weight(1f)
.size(28.dp)
)
}
}
PlayerBottomSheet(
playerState = playerState,
layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound * 0.9f),
onGlobalRouteEmitted = layoutState.collapse,
padding = layoutState.upperBound * 0.1f,
song = song,
Spacer(
modifier = Modifier
.align(Alignment.BottomCenter)
.weight(1f)
)
}
}