start construction audio player
This commit is contained in:
parent
2d5a377cd3
commit
df26c3c094
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = { })
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
// }
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue