roughly layout AudioPlayerView

This commit is contained in:
Christian Schabesberger 2024-09-21 13:09:41 +02:00
parent df26c3c094
commit a222a7c9a0
7 changed files with 332 additions and 71 deletions

View File

@ -23,8 +23,11 @@ package net.newpipe.newplayer.ui.audioplayer;
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -39,11 +42,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.NewPlayerUIState import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel import net.newpipe.newplayer.model.NewPlayerViewModel
import net.newpipe.newplayer.model.NewPlayerViewModelDummy
import net.newpipe.newplayer.ui.common.RepeatModeButton import net.newpipe.newplayer.ui.common.RepeatModeButton
import net.newpipe.newplayer.ui.common.ShuffleModeButton import net.newpipe.newplayer.ui.common.ShuffleModeButton
@ -56,48 +61,58 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
Box(modifier = Modifier.size(80.dp), contentAlignment = Alignment.Center) { Box(modifier = Modifier.size(80.dp), contentAlignment = Alignment.Center) {
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
uiState.currentPlaylistItemIndex != 0, uiState.currentPlaylistItemIndex != 0,
enter = fadeIn(animationSpec = tween(400)), enter = fadeIn(animationSpec = tween(400)),
exit = fadeOut(animationSpec = tween(400)) exit = fadeOut(animationSpec = tween(400))
) { ) {
IconButton( Button(
onClick = viewModel::toggleShuffle modifier = Modifier
.fillMaxSize()
.aspectRatio(1f),
onClick = {},
colors = lightAudioControlButtonColorScheme()
) { ) {
Icon( Image(
imageVector = Icons.Filled.SkipPrevious, modifier = Modifier.fillMaxSize(),
contentDescription = stringResource(R.string.widget_description_previous_stream) imageVector = Icons.Filled.SkipPrevious,
contentDescription = stringResource(R.string.widget_description_previous_stream)
) )
} }
} }
} }
Button( Button(
modifier = Modifier.size(80.dp), modifier = Modifier.size(80.dp),
onClick = if (uiState.playing) viewModel::pause else viewModel::play, onClick = if (uiState.playing) viewModel::pause else viewModel::play,
shape = CircleShape shape = CircleShape
) { ) {
Icon( Icon(
imageVector = if (uiState.playing) Icons.Filled.Pause else Icons.Filled.PlayArrow, imageVector = if (uiState.playing) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescription = stringResource( contentDescription = stringResource(
if (uiState.playing) R.string.widget_description_pause if (uiState.playing) R.string.widget_description_pause
else R.string.widget_description_play else R.string.widget_description_play
) )
) )
} }
Box(modifier = Modifier.size(80.dp), contentAlignment = Alignment.Center) { Box(modifier = Modifier.size(80.dp), contentAlignment = Alignment.Center) {
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
uiState.currentPlaylistItemIndex < uiState.playList.size - 1, uiState.currentPlaylistItemIndex < uiState.playList.size - 1,
enter = fadeIn(animationSpec = tween(400)), enter = fadeIn(animationSpec = tween(400)),
exit = fadeOut(animationSpec = tween(400)) exit = fadeOut(animationSpec = tween(400))
) { ) {
IconButton( Button(
onClick = viewModel::toggleShuffle modifier = Modifier
.fillMaxSize()
.aspectRatio(1f),
onClick = {},
colors = lightAudioControlButtonColorScheme()
) { ) {
Icon( Image(
imageVector = Icons.Filled.SkipNext, modifier = Modifier.fillMaxSize(),
contentDescription = stringResource(R.string.widget_description_next_stream) imageVector = Icons.Filled.SkipNext,
contentDescription = stringResource(R.string.widget_description_next_stream)
) )
} }
} }
@ -105,4 +120,14 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
RepeatModeButton(viewModel = viewModel, uiState = uiState) RepeatModeButton(viewModel = viewModel, uiState = uiState)
} }
}
@androidx.annotation.OptIn(UnstableApi::class)
@Preview(device = "id:pixel_6")
@Composable
fun AudioPlayerControllerPreview() {
// VideoPlayerTheme {
AudioPlaybackController(viewModel = NewPlayerViewModelDummy(), uiState = NewPlayerUIState.DUMMY)
// }
} }

View File

@ -22,14 +22,70 @@
package net.newpipe.newplayer.ui.audioplayer package net.newpipe.newplayer.ui.audioplayer
import android.app.Activity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel
import net.newpipe.newplayer.utils.getEmbeddedUiConfig
@androidx.annotation.OptIn(UnstableApi::class)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AudioPlayerTopBar(modifier: Modifier = Modifier) { fun AudioPlayerTopBar(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState
) {
val embeddedUiConfig =
if (LocalContext.current is Activity)
getEmbeddedUiConfig(activity = LocalContext.current as Activity)
else EmbeddedUiConfig.DUMMY
TopAppBar(modifier = modifier, TopAppBar(modifier = modifier,
title = { }) title = { }, actions = {
AnimatedVisibility(visible = uiState.chapters.isNotEmpty()) {
IconButton(
onClick = {
viewModel.openStreamSelection(
selectChapter = true,
embeddedUiConfig
)
},
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.MenuBook,
contentDescription = stringResource(R.string.widget_description_chapter_selection)
)
}
}
AnimatedVisibility(visible = 1 < uiState.playList.size) {
IconButton(
onClick = {
viewModel.openStreamSelection(
selectChapter = false,
embeddedUiConfig
)
},
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.List,
contentDescription = stringResource(R.string.widget_descriptoin_playlist_item_selection)
)
}
}
})
} }

View File

@ -22,29 +22,15 @@
package net.newpipe.newplayer.ui.audioplayer package net.newpipe.newplayer.ui.audioplayer
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ButtonDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.filled.ShuffleOn
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -61,11 +47,15 @@ import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel import net.newpipe.newplayer.model.NewPlayerViewModel
import net.newpipe.newplayer.model.NewPlayerViewModelDummy import net.newpipe.newplayer.model.NewPlayerViewModelDummy
import net.newpipe.newplayer.ui.common.NewPlayerSeeker import net.newpipe.newplayer.ui.common.NewPlayerSeeker
import net.newpipe.newplayer.ui.common.RepeatModeButton
import net.newpipe.newplayer.ui.common.ShuffleModeButton
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.getInsets import net.newpipe.newplayer.utils.getInsets
@Composable
fun lightAudioControlButtonColorScheme() = ButtonDefaults.buttonColors().copy(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface
)
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable
fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) { fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
@ -73,7 +63,7 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
Scaffold(modifier = Modifier Scaffold(modifier = Modifier
.fillMaxSize() .fillMaxSize()
.windowInsetsPadding(insets), .windowInsetsPadding(insets),
topBar = { AudioPlayerTopBar() }) { innerPadding -> topBar = { AudioPlayerTopBar(viewModel = viewModel, uiState = uiState) }) { innerPadding ->
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -82,35 +72,58 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Card( Column(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), modifier = Modifier
.fillMaxSize()
.padding(20.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Thumbnail( Box(modifier = Modifier
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri, .fillMaxSize()
contentDescription = stringResource( .weight(1f))
id = R.string.stream_thumbnail Box {
), Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Thumbnail(
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
),
)
}
}
Box(modifier = Modifier
.fillMaxSize()
.weight(1f))
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.title.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 6.em
) )
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.artist.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 4.em
)
Box(modifier = Modifier
.fillMaxSize()
.weight(0.2f))
NewPlayerSeeker(viewModel = viewModel, uiState = uiState)
Box(modifier = Modifier
.fillMaxSize()
.weight(0.2f))
AudioPlaybackController(viewModel = viewModel, uiState = uiState)
Box(modifier = Modifier
.fillMaxSize()
.weight(0.2f))
} }
Text( AudioBottomUI(viewModel, uiState)
text = uiState.currentlyPlaying?.mediaMetadata?.title.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 6.em
)
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.artist.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 4.em
)
NewPlayerSeeker(viewModel = viewModel, uiState = uiState)
AudioPlaybackController(viewModel = viewModel, uiState = uiState)
} }
} }
} }
@ -119,7 +132,7 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Preview(device = "id:pixel_6") @Preview(device = "id:pixel_6")
@Composable @Composable
fun AudioPlayerUIPreviewEmbedded() { fun AudioPlayerUIPreview() {
// VideoPlayerTheme { // VideoPlayerTheme {
AudioPlayerUI(viewModel = NewPlayerViewModelDummy(), uiState = NewPlayerUIState.DUMMY) AudioPlayerUI(viewModel = NewPlayerViewModelDummy(), uiState = NewPlayerUIState.DUMMY)
// } // }

View File

@ -0,0 +1,161 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer.ui.audioplayer
import androidx.annotation.OptIn
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Notes
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.LiveTv
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PictureInPicture
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Translate
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel
import net.newpipe.newplayer.model.NewPlayerViewModelDummy
@OptIn(UnstableApi::class)
@Composable
fun AudioBottomUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f), horizontalArrangement = Arrangement.SpaceAround
) {
Button(
onClick = { /*TODO*/ },
colors = lightAudioControlButtonColorScheme()
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Notes,
contentDescription = stringResource(
id = R.string.details_view_button_description
)
)
}
Button(onClick = { /*TODO*/ }, colors = lightAudioControlButtonColorScheme()) {
Icon(
imageVector = Icons.Filled.LiveTv,
contentDescription = stringResource(
id = R.string.fullscreen_button_description
)
)
}
Button(onClick = { /*TODO*/ }, colors = lightAudioControlButtonColorScheme()) {
Icon(
imageVector = Icons.Filled.PictureInPicture,
contentDescription = stringResource(
id = R.string.pip_button_description
)
)
}
}
Menu()
}
}
@Composable
private fun Menu() {
var showMenu: Boolean by remember { mutableStateOf(false) }
Box {
IconButton(onClick = {
showMenu = true
}) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.menu_item_more_settings)
)
}
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
DropdownMenuItem(text = { Text(stringResource(R.string.menu_item_playback_speed)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Speed,
contentDescription = stringResource(R.string.menu_item_playback_speed)
)
},
onClick = { /*TODO*/ showMenu = false })
DropdownMenuItem(text = { Text(stringResource(R.string.menu_item_language)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Translate,
contentDescription = stringResource(R.string.menu_item_language)
)
},
onClick = { /*TODO*/ showMenu = false })
DropdownMenuItem(text = { Text(stringResource(R.string.menu_item_share_timestamp)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Share,
contentDescription = stringResource(R.string.menu_item_share_timestamp)
)
},
onClick = { /*TODO*/ showMenu = false })
DropdownMenuItem(text = { Text(stringResource(R.string.menu_item_open_in_browser)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Language,
contentDescription = stringResource(R.string.menu_item_open_in_browser)
)
},
onClick = { /*TODO*/ showMenu = false })
}
}
}
@OptIn(UnstableApi::class)
@Preview(device = "id:pixel_6")
@Composable
fun AudioBottomUIPreview() {
Box(modifier = Modifier.fillMaxWidth()) {
AudioBottomUI(viewModel = NewPlayerViewModelDummy(), uiState = NewPlayerUIState.DUMMY)
}
}

View File

@ -144,6 +144,7 @@ fun DropDownMenu(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
// Preview // Preview
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
@OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=1080px,dpi=440,orientation=landscape") @Preview(device = "spec:width=1080px,height=1080px,dpi=440,orientation=landscape")
@Composable @Composable
fun VideoPlayerControllerDropDownPreview() { fun VideoPlayerControllerDropDownPreview() {

View File

@ -57,6 +57,7 @@ import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.theme.video_player_onSurface import net.newpipe.newplayer.ui.theme.video_player_onSurface
import net.newpipe.newplayer.utils.getEmbeddedUiConfig import net.newpipe.newplayer.utils.getEmbeddedUiConfig
@OptIn(UnstableApi::class)
@Composable @Composable
fun TopUI( fun TopUI(
modifier: Modifier, viewModel: NewPlayerViewModel, uiState: NewPlayerUIState modifier: Modifier, viewModel: NewPlayerViewModel, uiState: NewPlayerUIState

View File

@ -28,8 +28,9 @@
<string name="menu_item_fit_screen">Fit screen</string> <string name="menu_item_fit_screen">Fit screen</string>
<string name="menu_item_sub_titles">Subtitles</string> <string name="menu_item_sub_titles">Subtitles</string>
<string name="menu_item_language">Language</string> <string name="menu_item_language">Language</string>
<string name="menu_item_playback_speed">Playback speed</string>
<string name="widget_description_previous_stream">Previous stream</string> <string name="widget_description_previous_stream">Previous stream</string>
<string name="widget_description_next_stream">Previous stream</string> <string name="widget_description_next_stream">Next stream</string>
<string name="widget_description_play">Play</string> <string name="widget_description_play">Play</string>
<string name="widget_description_pause">Pause</string> <string name="widget_description_pause">Pause</string>
<string name="widget_description_toggle_fullscreen">Toggle fullscreen</string> <string name="widget_description_toggle_fullscreen">Toggle fullscreen</string>
@ -54,4 +55,7 @@
<string name="store_playlist">Save current playlist</string> <string name="store_playlist">Save current playlist</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="stream_thumbnail">Stream Thumbnail</string> <string name="stream_thumbnail">Stream Thumbnail</string>
<string name="details_view_button_description">Switch to details view</string>
<string name="fullscreen_button_description">Switch to fullscreen video mode</string>
<string name="pip_button_description">Switch to picture in picture mode</string>
</resources> </resources>