diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlaybackController.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlaybackController.kt index 07a546a..ea4ed9a 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlaybackController.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlaybackController.kt @@ -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) +// } } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerTopBar.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerTopBar.kt index 2276ef1..1c9890d 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerTopBar.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerTopBar.kt @@ -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) + ) + } + + } + }) } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerUI.kt index 97a6c76..b37f285 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerUI.kt @@ -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) // } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/BottomUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/BottomUI.kt new file mode 100644 index 0000000..c1aeb0e --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/BottomUI.kt @@ -0,0 +1,161 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * 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 . + */ + + + +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) + } +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/Menu.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/Menu.kt index d2a640e..a52dbd7 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/Menu.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/Menu.kt @@ -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() { diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/TopUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/TopUI.kt index 99dceb4..29c5d23 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/TopUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/TopUI.kt @@ -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 diff --git a/new-player/src/main/res/values/strings.xml b/new-player/src/main/res/values/strings.xml index 8b9454c..a21ed84 100644 --- a/new-player/src/main/res/values/strings.xml +++ b/new-player/src/main/res/values/strings.xml @@ -28,8 +28,9 @@ Fit screen Subtitles Language + Playback speed Previous stream - Previous stream + Next stream Play Pause Toggle fullscreen @@ -54,4 +55,7 @@ Save current playlist Close Stream Thumbnail + Switch to details view + Switch to fullscreen video mode + Switch to picture in picture mode \ No newline at end of file