start construction audio player

This commit is contained in:
Christian Schabesberger 2024-09-18 15:38:22 +02:00
parent 2d5a377cd3
commit df26c3c094
11 changed files with 466 additions and 96 deletions

View File

@ -1,3 +1,25 @@
/* 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 package net.newpipe.newplayer.ui
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box

View File

@ -0,0 +1,108 @@
/* 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.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.Row
import androidx.compose.foundation.layout.size
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.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.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.ui.common.RepeatModeButton
import net.newpipe.newplayer.ui.common.ShuffleModeButton
@androidx.annotation.OptIn(UnstableApi::class)
@OptIn(UnstableApi::class)
@Composable
fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
Row(verticalAlignment = Alignment.CenterVertically) {
ShuffleModeButton(viewModel = viewModel, uiState = uiState)
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))
) {
IconButton(
onClick = viewModel::toggleShuffle
) {
Icon(
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
) {
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
)
)
}
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))
) {
IconButton(
onClick = viewModel::toggleShuffle
) {
Icon(
imageVector = Icons.Filled.SkipNext,
contentDescription = stringResource(R.string.widget_description_next_stream)
)
}
}
}
RepeatModeButton(viewModel = viewModel, uiState = uiState)
}
}

View File

@ -0,0 +1,35 @@
/* 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.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AudioPlayerTopBar(modifier: Modifier = Modifier) {
TopAppBar(modifier = modifier,
title = { })
}

View File

@ -1,27 +1,126 @@
/* 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 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.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.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
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.model.NewPlayerViewModelDummy
import net.newpipe.newplayer.ui.NewPlayerUI import net.newpipe.newplayer.ui.common.NewPlayerSeeker
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 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
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable
fun AudioPlayerUI(viewModel:NewPlayerViewModel, uiState: NewPlayerUIState) { fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
val insets = getInsets()
Scaffold(modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(insets),
topBar = { AudioPlayerTopBar() }) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Thumbnail(
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
),
)
}
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)
}
}
}
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=700px,dpi=440,orientation=landscape") @Preview(device = "id:pixel_6")
@Composable @Composable
fun AudioPlayerUIPreviewEmbedded() { fun AudioPlayerUIPreviewEmbedded() {
VideoPlayerTheme { // VideoPlayerTheme {
AudioPlayerUI(viewModel = NewPlayerViewModelDummy(), uiState = NewPlayerUIState.DUMMY) AudioPlayerUI(viewModel = NewPlayerViewModelDummy(), uiState = NewPlayerUIState.DUMMY)
} // }
} }

View File

@ -0,0 +1,90 @@
/* 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.common
import android.util.Log
import androidx.annotation.OptIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel
import net.newpipe.newplayer.ui.seeker.ChapterSegment
import net.newpipe.newplayer.ui.seeker.DefaultSeekerColor
import net.newpipe.newplayer.ui.seeker.Seeker
import net.newpipe.newplayer.ui.seeker.SeekerColors
private const val TAG = "NewPlayerSeeker"
@OptIn(UnstableApi::class)
@Composable
fun NewPlayerSeeker(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState
) {
Seeker(
modifier = modifier,
value = uiState.seekerPosition,
onValueChange = viewModel::seekPositionChanged,
onValueChangeFinished = viewModel::seekingFinished,
readAheadValue = uiState.bufferedPercentage,
colors = customizedSeekerColors(),
chapterSegments = getSeekerSegmentsFromChapters(uiState.chapters, uiState.durationInMs)
)
}
@Composable
private fun customizedSeekerColors(): SeekerColors {
val colors = DefaultSeekerColor(
progressColor = MaterialTheme.colorScheme.primary,
thumbColor = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
readAheadColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
disabledProgressColor = MaterialTheme.colorScheme.primary,
disabledThumbColor = MaterialTheme.colorScheme.primary,
disabledTrackColor = MaterialTheme.colorScheme.primary
)
return colors
}
private fun getSeekerSegmentsFromChapters(chapters: List<Chapter>, duration: Long) =
chapters
.filter { chapter ->
if (chapter.chapterStartInMs in 1..<duration) {
true
} else {
Log.e(
TAG,
"Chapter mark outside of stream duration range: chapter: ${chapter.chapterTitle}, mark in ms: ${chapter.chapterStartInMs}, video duration in ms: ${duration}"
)
false
}
}
.map { chapter ->
val markPosition = chapter.chapterStartInMs.toFloat() / duration.toFloat()
ChapterSegment(name = chapter.chapterTitle ?: "", start = markPosition)
}

View File

@ -0,0 +1,84 @@
/* 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.common
import androidx.annotation.OptIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.RepeatOn
import androidx.compose.material.icons.filled.RepeatOneOn
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.filled.ShuffleOn
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R
import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel
@OptIn(UnstableApi::class)
@Composable
fun RepeatModeButton(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
IconButton(
onClick = viewModel::cycleRepeatMode
) {
when (uiState.repeatMode) {
RepeatMode.DO_NOT_REPEAT -> Icon(
imageVector = Icons.Filled.Repeat,
contentDescription = stringResource(R.string.repeat_mode_no_repeat)
)
RepeatMode.REPEAT_ALL -> Icon(
imageVector = Icons.Filled.RepeatOn,
contentDescription = stringResource(R.string.repeat_mode_repeat_all)
)
RepeatMode.REPEAT_ONE -> Icon(
imageVector = Icons.Filled.RepeatOneOn,
contentDescription = stringResource(R.string.repeat_mode_repeat_all)
)
}
}
}
@OptIn(UnstableApi::class)
@Composable
fun ShuffleModeButton(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
IconButton(
onClick = viewModel::toggleShuffle
) {
if (uiState.shuffleEnabled) {
Icon(
imageVector = Icons.Filled.ShuffleOn,
contentDescription = stringResource(R.string.shuffle_off)
)
} else {
Icon(
imageVector = Icons.Filled.Shuffle,
contentDescription = stringResource(R.string.shuffle_on)
)
}
}
}

View File

@ -27,8 +27,6 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Repeat import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.RepeatOn import androidx.compose.material.icons.filled.RepeatOn
import androidx.compose.material.icons.filled.RepeatOneOn import androidx.compose.material.icons.filled.RepeatOneOn
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.filled.ShuffleOn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -48,6 +46,8 @@ import net.newpipe.newplayer.RepeatMode
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.model.NewPlayerViewModelDummy
import net.newpipe.newplayer.ui.common.RepeatModeButton
import net.newpipe.newplayer.ui.common.ShuffleModeButton
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.getLocale import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getPlaylistDurationInMS import net.newpipe.newplayer.utils.getPlaylistDurationInMS
@ -77,42 +77,9 @@ fun StreamSelectTopBar(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, actions = { }, actions = {
IconButton( RepeatModeButton(viewModel = viewModel, uiState = uiState)
onClick = viewModel::cycleRepeatMode
) {
when (uiState.repeatMode) {
RepeatMode.DO_NOT_REPEAT -> Icon(
imageVector = Icons.Filled.Repeat,
contentDescription = stringResource(R.string.repeat_mode_no_repeat)
)
RepeatMode.REPEAT_ALL -> Icon( ShuffleModeButton(viewModel = viewModel, uiState = uiState)
imageVector = Icons.Filled.RepeatOn,
contentDescription = stringResource(R.string.repeat_mode_repeat_all)
)
RepeatMode.REPEAT_ONE -> Icon(
imageVector = Icons.Filled.RepeatOneOn,
contentDescription = stringResource(R.string.repeat_mode_repeat_all)
)
}
}
IconButton(
onClick = viewModel::toggleShuffle
) {
if (uiState.shuffleEnabled) {
Icon(
imageVector = Icons.Filled.ShuffleOn,
contentDescription = stringResource(R.string.shuffle_off)
)
} else {
Icon(
imageVector = Icons.Filled.Shuffle,
contentDescription = stringResource(R.string.shuffle_on)
)
}
}
IconButton( IconButton(
onClick = viewModel::onStorePlaylist onClick = viewModel::onStorePlaylist

View File

@ -164,6 +164,7 @@ fun ReorderableStreamItemsList(
} }
} }
@OptIn(UnstableApi::class)
@Preview(device = "id:pixel_5") @Preview(device = "id:pixel_5")
@Composable @Composable
fun VideoPlayerChannelSelectUIPreview() { fun VideoPlayerChannelSelectUIPreview() {

View File

@ -21,7 +21,7 @@
package net.newpipe.newplayer.ui.videoplayer.controller package net.newpipe.newplayer.ui.videoplayer.controller
import android.app.Activity import android.app.Activity
import android.util.Log import androidx.annotation.OptIn
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -29,7 +29,6 @@ import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.FullscreenExit import androidx.compose.material.icons.filled.FullscreenExit
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -39,17 +38,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import net.newpipe.newplayer.Chapter import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.EmbeddedUiConfig import net.newpipe.newplayer.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.UIModeState import net.newpipe.newplayer.model.UIModeState
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.model.NewPlayerViewModelDummy
import net.newpipe.newplayer.ui.seeker.ChapterSegment import net.newpipe.newplayer.ui.common.NewPlayerSeeker
import net.newpipe.newplayer.ui.seeker.DefaultSeekerColor
import net.newpipe.newplayer.ui.seeker.Seeker
import net.newpipe.newplayer.ui.seeker.SeekerColors
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.getEmbeddedUiConfig import net.newpipe.newplayer.utils.getEmbeddedUiConfig
import net.newpipe.newplayer.utils.getLocale import net.newpipe.newplayer.utils.getLocale
@ -58,6 +54,7 @@ import net.newpipe.newplayer.utils.getTimeStringFromMs
private const val TAG = "BottomUI" private const val TAG = "BottomUI"
@OptIn(UnstableApi::class)
@Composable @Composable
fun BottomUI( fun BottomUI(
modifier: Modifier, viewModel: NewPlayerViewModel, uiState: NewPlayerUIState modifier: Modifier, viewModel: NewPlayerViewModel, uiState: NewPlayerUIState
@ -69,15 +66,8 @@ fun BottomUI(
) { ) {
val locale = getLocale()!! val locale = getLocale()!!
Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: locale)) Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: locale))
Seeker(
Modifier.weight(1F), NewPlayerSeeker(modifier = Modifier.weight(1F), viewModel = viewModel, uiState = uiState)
value = uiState.seekerPosition,
onValueChange = viewModel::seekPositionChanged,
onValueChangeFinished = viewModel::seekingFinished,
readAheadValue = uiState.bufferedPercentage,
colors = customizedSeekerColors(),
chapterSegments = getSeekerSegmentsFromChapters(uiState.chapters, uiState.durationInMs)
)
Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: locale)) Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: locale))
@ -104,43 +94,12 @@ fun BottomUI(
} }
@Composable
private fun customizedSeekerColors(): SeekerColors {
val colors = DefaultSeekerColor(
progressColor = MaterialTheme.colorScheme.primary,
thumbColor = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
readAheadColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
disabledProgressColor = MaterialTheme.colorScheme.primary,
disabledThumbColor = MaterialTheme.colorScheme.primary,
disabledTrackColor = MaterialTheme.colorScheme.primary
)
return colors
}
private fun getSeekerSegmentsFromChapters(chapters: List<Chapter>, duration: Long) =
chapters
.filter { chapter ->
if (chapter.chapterStartInMs in 1..<duration) {
true
} else {
Log.e(
TAG,
"Chapter mark outside of stream duration range: chapter: ${chapter.chapterTitle}, mark in ms: ${chapter.chapterStartInMs}, video duration in ms: ${duration}"
)
false
}
}
.map { chapter ->
val markPosition = chapter.chapterStartInMs.toFloat() / duration.toFloat()
ChapterSegment(name = chapter.chapterTitle ?: "", start = markPosition)
}
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
// Preview // Preview
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
@OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") @Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
@Composable @Composable
fun VideoPlayerControllerBottomUIPreview() { fun VideoPlayerControllerBottomUIPreview() {

View File

@ -20,6 +20,7 @@
package net.newpipe.newplayer.ui.videoplayer.controller package net.newpipe.newplayer.ui.videoplayer.controller
import androidx.annotation.OptIn
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
@ -46,12 +47,14 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview 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 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.model.NewPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@OptIn(UnstableApi::class)
@Composable @Composable
fun CenterUI( fun CenterUI(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -71,7 +74,7 @@ fun CenterUI(
exit = fadeOut(animationSpec = tween(400)) exit = fadeOut(animationSpec = tween(400))
) { ) {
CenterControllButton( CenterControlButton(
buttonModifier = Modifier.fillMaxSize(), buttonModifier = Modifier.fillMaxSize(),
iconModifier = Modifier.size(40.dp), iconModifier = Modifier.size(40.dp),
icon = Icons.Filled.SkipPrevious, icon = Icons.Filled.SkipPrevious,
@ -81,7 +84,7 @@ fun CenterUI(
} }
} }
CenterControllButton( CenterControlButton(
buttonModifier = Modifier.size(80.dp), buttonModifier = Modifier.size(80.dp),
iconModifier = Modifier.size(60.dp), iconModifier = Modifier.size(60.dp),
icon = if (uiState.playing) Icons.Filled.Pause else Icons.Filled.PlayArrow, icon = if (uiState.playing) Icons.Filled.Pause else Icons.Filled.PlayArrow,
@ -97,7 +100,7 @@ fun CenterUI(
enter = fadeIn(animationSpec = tween(400)), enter = fadeIn(animationSpec = tween(400)),
exit = fadeOut(animationSpec = tween(400)) exit = fadeOut(animationSpec = tween(400))
) { ) {
CenterControllButton( CenterControlButton(
buttonModifier = Modifier.fillMaxSize(), buttonModifier = Modifier.fillMaxSize(),
iconModifier = Modifier.size(40.dp), iconModifier = Modifier.size(40.dp),
icon = Icons.Filled.SkipNext, icon = Icons.Filled.SkipNext,
@ -109,8 +112,9 @@ fun CenterUI(
} }
} }
@Composable @Composable
private fun CenterControllButton( private fun CenterControlButton(
buttonModifier: Modifier, buttonModifier: Modifier,
iconModifier: Modifier, iconModifier: Modifier,
icon: ImageVector, icon: ImageVector,

View File

@ -53,4 +53,5 @@
<string name="shuffle_off">Shuffle playlist disabled</string> <string name="shuffle_off">Shuffle playlist disabled</string>
<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>
</resources> </resources>