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.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
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.shape.CircleShape
import androidx.compose.material.icons.Icons
@ -39,11 +42,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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
import net.newpipe.newplayer.ui.common.RepeatModeButton
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) {
androidx.compose.animation.AnimatedVisibility(
uiState.currentPlaylistItemIndex != 0,
enter = fadeIn(animationSpec = tween(400)),
exit = fadeOut(animationSpec = tween(400))
uiState.currentPlaylistItemIndex != 0,
enter = fadeIn(animationSpec = tween(400)),
exit = fadeOut(animationSpec = tween(400))
) {
IconButton(
onClick = viewModel::toggleShuffle
Button(
modifier = Modifier
.fillMaxSize()
.aspectRatio(1f),
onClick = {},
colors = lightAudioControlButtonColorScheme()
) {
Icon(
imageVector = Icons.Filled.SkipPrevious,
contentDescription = stringResource(R.string.widget_description_previous_stream)
Image(
modifier = Modifier.fillMaxSize(),
imageVector = Icons.Filled.SkipPrevious,
contentDescription = stringResource(R.string.widget_description_previous_stream)
)
}
}
}
Button(
modifier = Modifier.size(80.dp),
onClick = if (uiState.playing) viewModel::pause else viewModel::play,
shape = CircleShape
modifier = Modifier.size(80.dp),
onClick = if (uiState.playing) viewModel::pause else viewModel::play,
shape = CircleShape
) {
Icon(
imageVector = if (uiState.playing) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescription = stringResource(
if (uiState.playing) R.string.widget_description_pause
else R.string.widget_description_play
imageVector = if (uiState.playing) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescription = stringResource(
if (uiState.playing) R.string.widget_description_pause
else R.string.widget_description_play
)
)
}
Box(modifier = Modifier.size(80.dp), contentAlignment = Alignment.Center) {
androidx.compose.animation.AnimatedVisibility(
uiState.currentPlaylistItemIndex < uiState.playList.size - 1,
enter = fadeIn(animationSpec = tween(400)),
exit = fadeOut(animationSpec = tween(400))
uiState.currentPlaylistItemIndex < uiState.playList.size - 1,
enter = fadeIn(animationSpec = tween(400)),
exit = fadeOut(animationSpec = tween(400))
) {
IconButton(
onClick = viewModel::toggleShuffle
Button(
modifier = Modifier
.fillMaxSize()
.aspectRatio(1f),
onClick = {},
colors = lightAudioControlButtonColorScheme()
) {
Icon(
imageVector = Icons.Filled.SkipNext,
contentDescription = stringResource(R.string.widget_description_next_stream)
Image(
modifier = Modifier.fillMaxSize(),
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)
}
}
@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
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
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)
@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,
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
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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape
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.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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.NewPlayerViewModelDummy
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.getInsets
@Composable
fun lightAudioControlButtonColorScheme() = ButtonDefaults.buttonColors().copy(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface
)
@OptIn(UnstableApi::class)
@Composable
fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
@ -73,7 +63,7 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
Scaffold(modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(insets),
topBar = { AudioPlayerTopBar() }) { innerPadding ->
topBar = { AudioPlayerTopBar(viewModel = viewModel, uiState = uiState) }) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
@ -82,35 +72,58 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Thumbnail(
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
),
Box(modifier = Modifier
.fillMaxSize()
.weight(1f))
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(
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)
AudioBottomUI(viewModel, uiState)
}
}
}
@ -119,7 +132,7 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
@OptIn(UnstableApi::class)
@Preview(device = "id:pixel_6")
@Composable
fun AudioPlayerUIPreviewEmbedded() {
fun AudioPlayerUIPreview() {
// VideoPlayerTheme {
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
///////////////////////////////////////////////////////////////////
@OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=1080px,dpi=440,orientation=landscape")
@Composable
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.utils.getEmbeddedUiConfig
@OptIn(UnstableApi::class)
@Composable
fun TopUI(
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_sub_titles">Subtitles</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_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_pause">Pause</string>
<string name="widget_description_toggle_fullscreen">Toggle fullscreen</string>
@ -54,4 +55,7 @@
<string name="store_playlist">Save current playlist</string>
<string name="close">Close</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>