From 86ff26b34a5cc701c76f8440e789046d809cd8a7 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 23 Jul 2023 20:56:31 +0800 Subject: [PATCH] Adjust tempo --- .../com/zionhuang/music/ui/menu/PlayerMenu.kt | 387 ++++++++++++++++++ .../com/zionhuang/music/ui/player/Queue.kt | 280 +------------ .../main/res/drawable/slow_motion_video.xml | 9 + 3 files changed, 397 insertions(+), 279 deletions(-) create mode 100644 app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt create mode 100644 app/src/main/res/drawable/slow_motion_video.xml diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt new file mode 100644 index 000000000..057e75b66 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/menu/PlayerMenu.kt @@ -0,0 +1,387 @@ +package com.zionhuang.music.ui.menu + +import android.content.Intent +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.media.audiofx.AudioEffect +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.net.toUri +import androidx.media3.common.PlaybackParameters +import androidx.media3.exoplayer.offline.DownloadRequest +import androidx.media3.exoplayer.offline.DownloadService +import androidx.navigation.NavController +import com.zionhuang.music.LocalDatabase +import com.zionhuang.music.LocalDownloadUtil +import com.zionhuang.music.R +import com.zionhuang.music.constants.ListItemHeight +import com.zionhuang.music.db.entities.PlaylistSongMap +import com.zionhuang.music.models.MediaMetadata +import com.zionhuang.music.playback.ExoDownloadService +import com.zionhuang.music.playback.PlayerConnection +import com.zionhuang.music.ui.component.BigSeekBar +import com.zionhuang.music.ui.component.BottomSheetState +import com.zionhuang.music.ui.component.DownloadGridMenu +import com.zionhuang.music.ui.component.GridMenu +import com.zionhuang.music.ui.component.GridMenuItem +import com.zionhuang.music.ui.component.ListDialog +import kotlin.math.log2 +import kotlin.math.pow +import kotlin.math.round + +@Composable +fun PlayerMenu( + mediaMetadata: MediaMetadata?, + navController: NavController, + playerBottomSheetState: BottomSheetState, + playerConnection: PlayerConnection, + onShowDetailsDialog: () -> Unit, + onDismiss: () -> Unit, +) { + mediaMetadata ?: return + val context = LocalContext.current + val database = LocalDatabase.current + val localConfiguration = LocalConfiguration.current + val playerVolume = playerConnection.service.playerVolume.collectAsState() + val activityResultLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { } + + val download by LocalDownloadUtil.current.getDownload(mediaMetadata.id).collectAsState(initial = null) + + var showChoosePlaylistDialog by rememberSaveable { + mutableStateOf(false) + } + + AddToPlaylistDialog( + isVisible = showChoosePlaylistDialog, + onAdd = { playlist -> + database.transaction { + insert(mediaMetadata) + insert( + PlaylistSongMap( + songId = mediaMetadata.id, + playlistId = playlist.id, + position = playlist.songCount + ) + ) + } + }, + onDismiss = { + showChoosePlaylistDialog = false + } + ) + + var showSelectArtistDialog by rememberSaveable { + mutableStateOf(false) + } + + if (showSelectArtistDialog) { + ListDialog( + onDismiss = { showSelectArtistDialog = false } + ) { + items(mediaMetadata.artists) { artist -> + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .fillParentMaxWidth() + .height(ListItemHeight) + .clickable { + navController.navigate("artist/${artist.id}") + showSelectArtistDialog = false + playerBottomSheetState.collapseSoft() + onDismiss() + } + .padding(horizontal = 24.dp), + ) { + Text( + text = artist.name, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + + var tempo by remember { + mutableStateOf(playerConnection.player.playbackParameters.speed) + } + var transposeValue by remember { + mutableStateOf(round(12 * log2(playerConnection.player.playbackParameters.pitch)).toInt()) + } + val updatePlaybackParameters = { + playerConnection.player.playbackParameters = PlaybackParameters(tempo, 2f.pow(transposeValue.toFloat() / 12)) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(top = 24.dp, bottom = 6.dp) + ) { + Icon( + painter = painterResource(R.drawable.volume_up), + contentDescription = null, + modifier = Modifier.size(28.dp) + ) + + BigSeekBar( + progressProvider = playerVolume::value, + onProgressChange = { playerConnection.service.playerVolume.value = it }, + modifier = Modifier.weight(1f) + ) + } + + if (localConfiguration.orientation == ORIENTATION_LANDSCAPE) { + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 6.dp) + ) { + ValueAdjuster( + icon = R.drawable.slow_motion_video, + currentValue = tempo, + values = listOf(0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f), + onValueUpdate = { + tempo = it + updatePlaybackParameters() + }, + valueText = { "x$it" }, + modifier = Modifier.weight(1f) + ) + + ValueAdjuster( + icon = R.drawable.tune, + currentValue = transposeValue, + values = (-12..12).toList(), + onValueUpdate = { + transposeValue = it + updatePlaybackParameters() + }, + valueText = { "${if (it > 0) "+" else ""}$it" }, + modifier = Modifier.weight(1f) + ) + } + } else { + ValueAdjuster( + icon = R.drawable.slow_motion_video, + currentValue = tempo, + values = listOf(0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f), + onValueUpdate = { + tempo = it + updatePlaybackParameters() + }, + valueText = { "x$it" }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 6.dp) + ) + + ValueAdjuster( + icon = R.drawable.tune, + currentValue = transposeValue, + values = (-12..12).toList(), + onValueUpdate = { + transposeValue = it + updatePlaybackParameters() + }, + valueText = { "${if (it > 0) "+" else ""}$it" }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 6.dp) + ) + } + + GridMenu( + contentPadding = PaddingValues( + start = 8.dp, + top = 8.dp, + end = 8.dp, + bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + ) + ) { + GridMenuItem( + icon = R.drawable.radio, + title = R.string.start_radio + ) { + playerConnection.service.startRadioSeamlessly() + onDismiss() + } + GridMenuItem( + icon = R.drawable.playlist_add, + title = R.string.add_to_playlist + ) { + showChoosePlaylistDialog = true + } + DownloadGridMenu( + state = download?.state, + onDownload = { + database.transaction { + insert(mediaMetadata) + } + val downloadRequest = DownloadRequest.Builder(mediaMetadata.id, mediaMetadata.id.toUri()) + .setCustomCacheKey(mediaMetadata.id) + .setData(mediaMetadata.title.toByteArray()) + .build() + DownloadService.sendAddDownload( + context, + ExoDownloadService::class.java, + downloadRequest, + false + ) + }, + onRemoveDownload = { + DownloadService.sendRemoveDownload( + context, + ExoDownloadService::class.java, + mediaMetadata.id, + false + ) + } + ) + GridMenuItem( + icon = R.drawable.artist, + title = R.string.view_artist + ) { + if (mediaMetadata.artists.size == 1) { + navController.navigate("artist/${mediaMetadata.artists[0].id}") + playerBottomSheetState.collapseSoft() + onDismiss() + } else { + showSelectArtistDialog = true + } + } + if (mediaMetadata.album != null) { + GridMenuItem( + icon = R.drawable.album, + title = R.string.view_album + ) { + navController.navigate("album/${mediaMetadata.album.id}") + playerBottomSheetState.collapseSoft() + onDismiss() + } + } + GridMenuItem( + icon = R.drawable.share, + title = R.string.share + ) { + val intent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaMetadata.id}") + } + context.startActivity(Intent.createChooser(intent, null)) + onDismiss() + } + GridMenuItem( + icon = R.drawable.info, + title = R.string.details + ) { + onShowDetailsDialog() + onDismiss() + } + GridMenuItem( + icon = R.drawable.equalizer, + title = R.string.equalizer + ) { + val intent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { + putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playerConnection.player.audioSessionId) + putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) + putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) + } + if (intent.resolveActivity(context.packageManager) != null) { + activityResultLauncher.launch(intent) + } + onDismiss() + } + } +} + +@Composable +fun ValueAdjuster( + @DrawableRes icon: Int, + currentValue: T, + values: List, + onValueUpdate: (T) -> Unit, + valueText: (T) -> String, + modifier: Modifier, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + ) { + Icon( + painter = painterResource(icon), + contentDescription = null, + modifier = Modifier.size(28.dp) + ) + + IconButton( + enabled = currentValue != values.first(), + onClick = { + onValueUpdate(values[values.indexOf(currentValue) - 1]) + } + ) { + Icon( + painter = painterResource(R.drawable.remove), + contentDescription = null + ) + } + + Text( + text = valueText(currentValue), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f) + ) + + IconButton( + enabled = currentValue != values.last(), + onClick = { + onValueUpdate(values[values.indexOf(currentValue) + 1]) + } + ) { + Icon( + painter = painterResource(R.drawable.add), + contentDescription = null + ) + } + } +} diff --git a/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt b/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt index 7d7701d44..0719dc1b8 100644 --- a/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt +++ b/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt @@ -1,11 +1,7 @@ package com.zionhuang.music.ui.player -import android.content.Intent -import android.media.audiofx.AudioEffect import android.text.format.Formatter import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background @@ -14,7 +10,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -26,11 +21,9 @@ 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.sizeIn import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState @@ -71,41 +64,23 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.DialogProperties -import androidx.core.net.toUri -import androidx.media3.common.PlaybackParameters -import androidx.media3.exoplayer.offline.DownloadRequest -import androidx.media3.exoplayer.offline.DownloadService import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder import androidx.navigation.NavController -import com.zionhuang.music.LocalDatabase -import com.zionhuang.music.LocalDownloadUtil import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R import com.zionhuang.music.constants.ListItemHeight import com.zionhuang.music.constants.ShowLyricsKey -import com.zionhuang.music.db.entities.PlaylistSongMap import com.zionhuang.music.extensions.metadata import com.zionhuang.music.extensions.move import com.zionhuang.music.extensions.togglePlayPause -import com.zionhuang.music.models.MediaMetadata -import com.zionhuang.music.playback.ExoDownloadService -import com.zionhuang.music.playback.PlayerConnection -import com.zionhuang.music.ui.component.BigSeekBar import com.zionhuang.music.ui.component.BottomSheet import com.zionhuang.music.ui.component.BottomSheetState -import com.zionhuang.music.ui.component.DownloadGridMenu -import com.zionhuang.music.ui.component.GridMenu -import com.zionhuang.music.ui.component.GridMenuItem -import com.zionhuang.music.ui.component.ListDialog import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.MediaMetadataListItem -import com.zionhuang.music.ui.menu.AddToPlaylistDialog +import com.zionhuang.music.ui.menu.PlayerMenu import com.zionhuang.music.ui.utils.reordering.ReorderingLazyColumn import com.zionhuang.music.ui.utils.reordering.draggedItem import com.zionhuang.music.ui.utils.reordering.rememberReorderingState @@ -116,9 +91,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlin.math.log2 -import kotlin.math.pow -import kotlin.math.round import kotlin.math.roundToInt @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @@ -554,253 +526,3 @@ fun Queue( } } } - -@Composable -fun PlayerMenu( - mediaMetadata: MediaMetadata?, - navController: NavController, - playerBottomSheetState: BottomSheetState, - playerConnection: PlayerConnection, - onShowDetailsDialog: () -> Unit, - onDismiss: () -> Unit, -) { - mediaMetadata ?: return - val context = LocalContext.current - val database = LocalDatabase.current - val playerVolume = playerConnection.service.playerVolume.collectAsState() - val activityResultLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { } - - val download by LocalDownloadUtil.current.getDownload(mediaMetadata.id).collectAsState(initial = null) - - var showChoosePlaylistDialog by rememberSaveable { - mutableStateOf(false) - } - - AddToPlaylistDialog( - isVisible = showChoosePlaylistDialog, - onAdd = { playlist -> - database.transaction { - insert(mediaMetadata) - insert( - PlaylistSongMap( - songId = mediaMetadata.id, - playlistId = playlist.id, - position = playlist.songCount - ) - ) - } - }, - onDismiss = { - showChoosePlaylistDialog = false - } - ) - - var showSelectArtistDialog by rememberSaveable { - mutableStateOf(false) - } - - if (showSelectArtistDialog) { - ListDialog( - onDismiss = { showSelectArtistDialog = false } - ) { - items(mediaMetadata.artists) { artist -> - Box( - contentAlignment = Alignment.CenterStart, - modifier = Modifier - .fillParentMaxWidth() - .height(ListItemHeight) - .clickable { - navController.navigate("artist/${artist.id}") - showSelectArtistDialog = false - playerBottomSheetState.collapseSoft() - onDismiss() - } - .padding(horizontal = 24.dp), - ) { - Text( - text = artist.name, - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - } - - var transposeValue by remember { - mutableStateOf(round(12 * log2(playerConnection.player.playbackParameters.pitch)).toInt()) - } - - Row( - horizontalArrangement = Arrangement.spacedBy(24.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 24.dp) - ) { - Icon( - painter = painterResource(R.drawable.volume_up), - contentDescription = null, - modifier = Modifier.size(28.dp) - ) - - BigSeekBar( - progressProvider = playerVolume::value, - onProgressChange = { playerConnection.service.playerVolume.value = it }, - modifier = Modifier.weight(1f) - ) - } - - Row( - horizontalArrangement = Arrangement.spacedBy(24.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 6.dp) - ) { - Icon( - painter = painterResource(R.drawable.tune), - contentDescription = null, - modifier = Modifier.size(28.dp) - ) - - IconButton( - enabled = transposeValue > -12, - onClick = { - transposeValue-- - playerConnection.player.playbackParameters = PlaybackParameters(1f, 2f.pow(transposeValue.toFloat() / 12)) - } - ) { - Icon( - painter = painterResource(R.drawable.remove), - contentDescription = null - ) - } - - Text( - text = "${if (transposeValue > 0) "+" else ""}$transposeValue", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - modifier = Modifier.weight(1f) - ) - - IconButton( - enabled = transposeValue < 12, - onClick = { - transposeValue++ - playerConnection.player.playbackParameters = PlaybackParameters(1f, 2f.pow(transposeValue.toFloat() / 12)) - } - ) { - Icon( - painter = painterResource(R.drawable.add), - contentDescription = null - ) - } - } - - GridMenu( - contentPadding = PaddingValues( - start = 8.dp, - top = 8.dp, - end = 8.dp, - bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() - ) - ) { - GridMenuItem( - icon = R.drawable.radio, - title = R.string.start_radio - ) { - playerConnection.service.startRadioSeamlessly() - onDismiss() - } - GridMenuItem( - icon = R.drawable.playlist_add, - title = R.string.add_to_playlist - ) { - showChoosePlaylistDialog = true - } - DownloadGridMenu( - state = download?.state, - onDownload = { - database.transaction { - insert(mediaMetadata) - } - val downloadRequest = DownloadRequest.Builder(mediaMetadata.id, mediaMetadata.id.toUri()) - .setCustomCacheKey(mediaMetadata.id) - .setData(mediaMetadata.title.toByteArray()) - .build() - DownloadService.sendAddDownload( - context, - ExoDownloadService::class.java, - downloadRequest, - false - ) - }, - onRemoveDownload = { - DownloadService.sendRemoveDownload( - context, - ExoDownloadService::class.java, - mediaMetadata.id, - false - ) - } - ) - GridMenuItem( - icon = R.drawable.artist, - title = R.string.view_artist - ) { - if (mediaMetadata.artists.size == 1) { - navController.navigate("artist/${mediaMetadata.artists[0].id}") - playerBottomSheetState.collapseSoft() - onDismiss() - } else { - showSelectArtistDialog = true - } - } - if (mediaMetadata.album != null) { - GridMenuItem( - icon = R.drawable.album, - title = R.string.view_album - ) { - navController.navigate("album/${mediaMetadata.album.id}") - playerBottomSheetState.collapseSoft() - onDismiss() - } - } - GridMenuItem( - icon = R.drawable.share, - title = R.string.share - ) { - val intent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaMetadata.id}") - } - context.startActivity(Intent.createChooser(intent, null)) - onDismiss() - } - GridMenuItem( - icon = R.drawable.info, - title = R.string.details - ) { - onShowDetailsDialog() - onDismiss() - } - GridMenuItem( - icon = R.drawable.equalizer, - title = R.string.equalizer - ) { - val intent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { - putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playerConnection.player.audioSessionId) - putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) - putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) - } - if (intent.resolveActivity(context.packageManager) != null) { - activityResultLauncher.launch(intent) - } - onDismiss() - } - } -} diff --git a/app/src/main/res/drawable/slow_motion_video.xml b/app/src/main/res/drawable/slow_motion_video.xml new file mode 100644 index 000000000..69ed770ae --- /dev/null +++ b/app/src/main/res/drawable/slow_motion_video.xml @@ -0,0 +1,9 @@ + + +