make NewPlayer handle playlist updates and repository accesses

This commit is contained in:
Christian Schabesberger 2024-08-30 16:43:38 +02:00
parent 22d7bcf552
commit cdcfeaedd7
16 changed files with 729 additions and 319 deletions

View File

@ -0,0 +1,4 @@
kotlin version: 2.0.20-Beta2
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@ -23,8 +23,10 @@ package net.newpipe.newplayer
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -39,6 +41,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.playerInternals.PlayList import net.newpipe.newplayer.playerInternals.PlayList
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromItemList
import kotlin.Exception import kotlin.Exception
enum class PlayMode { enum class PlayMode {
@ -59,14 +63,14 @@ interface NewPlayer {
var playWhenReady: Boolean var playWhenReady: Boolean
val duration: Long val duration: Long
val bufferedPercentage: Int val bufferedPercentage: Int
val repository: MediaRepository
val sharingLinkWithOffsetPossible: Boolean val sharingLinkWithOffsetPossible: Boolean
var currentPosition: Long var currentPosition: Long
var fastSeekAmountSec: Int var fastSeekAmountSec: Int
var playBackMode: PlayMode var playBackMode: PlayMode
var playMode: StateFlow<PlayMode?> var playMode: StateFlow<PlayMode?>
var playlist: PlayList val playlist: PlayList
val playlistInPlaylistItems: StateFlow<List<PlaylistItem>>
// callbacks // callbacks
@ -87,17 +91,17 @@ interface NewPlayer {
private var preferredStreamVariants: List<String> = emptyList() private var preferredStreamVariants: List<String> = emptyList()
private var sharingLinkWithOffsetPossible = false private var sharingLinkWithOffsetPossible = false
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory) : Builder { fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory): Builder {
this.mediaSourceFactory = mediaSourceFactory this.mediaSourceFactory = mediaSourceFactory
return this return this
} }
fun setPreferredStreamVariants(preferredStreamVariants: List<String>) : Builder { fun setPreferredStreamVariants(preferredStreamVariants: List<String>): Builder {
this.preferredStreamVariants = preferredStreamVariants this.preferredStreamVariants = preferredStreamVariants
return this return this
} }
fun setSharingLinkWithOffsetPossible(possible: Boolean) : Builder { fun setSharingLinkWithOffsetPossible(possible: Boolean): Builder {
this.sharingLinkWithOffsetPossible = false this.sharingLinkWithOffsetPossible = false
return this return this
} }
@ -123,7 +127,7 @@ class NewPlayerImpl(
val app: Application, val app: Application,
override val internalPlayer: Player, override val internalPlayer: Player,
override val preferredStreamVariants: List<String>, override val preferredStreamVariants: List<String>,
override val repository: MediaRepository, private val repository: MediaRepository,
override val sharingLinkWithOffsetPossible: Boolean override val sharingLinkWithOffsetPossible: Boolean
) : NewPlayer { ) : NewPlayer {
@ -161,9 +165,14 @@ class NewPlayerImpl(
override val duration: Long override val duration: Long
get() = internalPlayer.duration get() = internalPlayer.duration
override var playlist = PlayList(internalPlayer) override val playlist = PlayList(internalPlayer)
val mutablePlaylistAsPlaylistItems = MutableStateFlow<List<PlaylistItem>>(emptyList())
override val playlistInPlaylistItems: StateFlow<List<PlaylistItem>> =
mutablePlaylistAsPlaylistItems.asStateFlow()
init { init {
println("gurken init")
internalPlayer.addListener(object : Player.Listener { internalPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) { override fun onPlayerError(error: PlaybackException) {
launchJobAndCollectError { launchJobAndCollectError {
@ -183,9 +192,28 @@ class NewPlayerImpl(
mutableOnEvent.emit(Pair(player, events)) mutableOnEvent.emit(Pair(player, events))
} }
} }
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
super.onTimelineChanged(timeline, reason)
updatePlaylistItems()
}
}) })
} }
private fun updatePlaylistItems() {
playerScope.launch {
val playlist = getPlaylistItemsFromItemList(playlist, repository)
var playlistDuration = 0
for (item in playlist) {
playlistDuration += item.lengthInS
}
mutablePlaylistAsPlaylistItems.update {
playlist
}
}
}
override fun prepare() { override fun prepare() {
internalPlayer.prepare() internalPlayer.prepare()
} }

View File

@ -20,8 +20,7 @@
package net.newpipe.newplayer.model package net.newpipe.newplayer.model
import android.os.Parcelable import androidx.media3.common.Player
import kotlinx.parcelize.Parcelize
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.playerInternals.PlaylistItem import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
@ -42,7 +41,10 @@ data class VideoPlayerUIState(
val brightness: Float?, // when null use system value val brightness: Float?, // when null use system value
val embeddedUiConfig: EmbeddedUiConfig?, val embeddedUiConfig: EmbeddedUiConfig?,
val playList: List<PlaylistItem>, val playList: List<PlaylistItem>,
val chapters: List<Chapter> val chapters: List<Chapter>,
val shuffleEnabled: Boolean,
val repeatMode: Int,
val playListDurationInS: Int
) { ) {
companion object { companion object {
val DEFAULT = VideoPlayerUIState( val DEFAULT = VideoPlayerUIState(
@ -64,7 +66,10 @@ data class VideoPlayerUIState(
brightness = null, brightness = null,
embeddedUiConfig = null, embeddedUiConfig = null,
playList = emptyList(), playList = emptyList(),
chapters = emptyList() chapters = emptyList(),
shuffleEnabled = false,
repeatMode = Player.REPEAT_MODE_OFF,
playListDurationInS = 0
) )
} }
} }

View File

@ -32,7 +32,6 @@ import net.newpipe.newplayer.utils.Thumbnail
interface VideoPlayerViewModel { interface VideoPlayerViewModel {
var newPlayer: NewPlayer? var newPlayer: NewPlayer?
val internalPlayer: Player?
val uiState: StateFlow<VideoPlayerUIState> val uiState: StateFlow<VideoPlayerUIState>
var minContentRatio: Float var minContentRatio: Float
var maxContentRatio: Float var maxContentRatio: Float
@ -61,4 +60,7 @@ interface VideoPlayerViewModel {
fun closeStreamSelection() fun closeStreamSelection()
fun chapterSelected(chapter: Chapter) fun chapterSelected(chapter: Chapter)
fun streamSelected(streamId: Int) fun streamSelected(streamId: Int)
fun setRepeatmode(repeatMode: Int)
fun setSuffleEnabled(enabled: Boolean)
fun onStorePlaylist()
} }

View File

@ -90,9 +90,6 @@ class VideoPlayerViewModelImpl @Inject constructor(
override val uiState = mutableUiState.asStateFlow() override val uiState = mutableUiState.asStateFlow()
override val internalPlayer: Player?
get() = newPlayer?.internalPlayer
override var minContentRatio: Float = 4F / 3F override var minContentRatio: Float = 4F / 3F
set(value) { set(value) {
if (value <= 0 || maxContentRatio < value) Log.e( if (value <= 0 || maxContentRatio < value) Log.e(
@ -133,7 +130,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
override val onBackPressed: SharedFlow<Unit> = mutableOnBackPressed.asSharedFlow() override val onBackPressed: SharedFlow<Unit> = mutableOnBackPressed.asSharedFlow()
private fun installNewPlayer() { private fun installNewPlayer() {
internalPlayer?.let { player -> newPlayer?.internalPlayer?.let { player ->
Log.d(TAG, "Install player: ${player.videoSize.width}") Log.d(TAG, "Install player: ${player.videoSize.width}")
player.addListener(object : Player.Listener { player.addListener(object : Player.Listener {
@ -158,11 +155,6 @@ class VideoPlayerViewModelImpl @Inject constructor(
it.copy(isLoading = isLoading) it.copy(isLoading = isLoading)
} }
} }
override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) {
super.onPlaylistMetadataChanged(mediaMetadata)
updatePlaylist()
}
}) })
} }
newPlayer?.let { newPlayer -> newPlayer?.let { newPlayer ->
@ -179,8 +171,12 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
} }
} }
viewModelScope.launch {
newPlayer.playlistInPlaylistItems.collect { playlist ->
mutableUiState.update { it.copy(playList = playlist) }
}
}
} }
updatePlaylist()
} }
fun updateContentRatio(videoSize: VideoSize) { fun updateContentRatio(videoSize: VideoSize) {
@ -266,9 +262,10 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
private fun updateProgressOnce() { private fun updateProgressOnce() {
val progress = internalPlayer?.currentPosition ?: 0 val progress = newPlayer?.currentPosition ?: 0
val duration = internalPlayer?.duration ?: 1 val duration = newPlayer?.duration ?: 1
val bufferedPercentage = (internalPlayer?.bufferedPercentage?.toFloat() ?: 0f) / 100f val bufferedPercentage =
(newPlayer?.bufferedPercentage?.toFloat() ?: 0f) / 100f
val progressPercentage = progress.toFloat() / duration.toFloat() val progressPercentage = progress.toFloat() / duration.toFloat()
mutableUiState.update { mutableUiState.update {
@ -297,13 +294,13 @@ class VideoPlayerViewModelImpl @Inject constructor(
override fun seekingFinished() { override fun seekingFinished() {
resetHideUiDelayedJob() resetHideUiDelayedJob()
val seekerPosition = mutableUiState.value.seekerPosition val seekerPosition = mutableUiState.value.seekerPosition
val seekPositionInMs = (internalPlayer?.duration?.toFloat() ?: 0F) * seekerPosition val seekPositionInMs = (newPlayer?.duration?.toFloat() ?: 0F) * seekerPosition
newPlayer?.currentPosition = seekPositionInMs.toLong() newPlayer?.currentPosition = seekPositionInMs.toLong()
Log.i(TAG, "Seek to Ms: $seekPositionInMs") Log.i(TAG, "Seek to Ms: $seekPositionInMs")
} }
override fun embeddedDraggedDown(offset: Float) { override fun embeddedDraggedDown(offset: Float) {
saveTryEmit(mutableEmbeddedPlayerDraggedDownBy, offset) safeTryEmit(mutableEmbeddedPlayerDraggedDownBy, offset)
} }
override fun fastSeek(count: Int) { override fun fastSeek(count: Int) {
@ -362,7 +359,6 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
override fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig) { override fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig) {
println("gurken openSelection ${embeddedUiConfig}")
uiVisibilityJob?.cancel() uiVisibilityJob?.cancel()
if (!uiState.value.uiMode.fullscreen) { if (!uiState.value.uiMode.fullscreen) {
this.embeddedUiConfig = embeddedUiConfig this.embeddedUiConfig = embeddedUiConfig
@ -388,7 +384,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
if (nextMode != null) { if (nextMode != null) {
updateUiMode(nextMode) updateUiMode(nextMode)
} else { } else {
saveTryEmit(mutableOnBackPressed, Unit) safeTryEmit(mutableOnBackPressed, Unit)
} }
} }
@ -408,6 +404,25 @@ class VideoPlayerViewModelImpl @Inject constructor(
println("stream selected: $streamId") println("stream selected: $streamId")
} }
override fun setRepeatmode(repeatMode: Int) {
assert(
repeatMode == Player.REPEAT_MODE_ALL
|| repeatMode == Player.REPEAT_MODE_OFF
|| repeatMode == Player.REPEAT_MODE_ONE
) {
"Illegal repeat mode: $repeatMode"
}
TODO("Not yet implemented")
}
override fun setSuffleEnabled(enabled: Boolean) {
TODO("Not yet implemented")
}
override fun onStorePlaylist() {
TODO("Not yet implemented")
}
private fun updateUiMode(newState: UIModeState) { private fun updateUiMode(newState: UIModeState) {
val newPlayMode = newState.toPlayMode() val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode() val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()
@ -420,7 +435,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
} }
private fun getEmbeddedUiRatio() = internalPlayer?.let { player -> private fun getEmbeddedUiRatio() = newPlayer?.internalPlayer?.let { player ->
val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio() val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio()
return (if (videoRatio.isNaN()) currentContentRatio return (if (videoRatio.isNaN()) currentContentRatio
else videoRatio).coerceIn(minContentRatio, maxContentRatio) else videoRatio).coerceIn(minContentRatio, maxContentRatio)
@ -428,21 +443,8 @@ class VideoPlayerViewModelImpl @Inject constructor(
} ?: minContentRatio } ?: minContentRatio
private fun updatePlaylist() { private fun <T> safeTryEmit(sharedFlow: MutableSharedFlow<T>, value: T) {
newPlayer?.let { newPlayer -> if (!sharedFlow.tryEmit(value)) {
viewModelScope.launch {
val playlist = getPlaylistItemsFromItemList(
newPlayer.playlist, newPlayer.repository
)
mutableUiState.update {
it.copy(playList = playlist)
}
}
}
}
private fun <T> saveTryEmit(sharedFlow: MutableSharedFlow<T>, value: T) {
if(sharedFlow.tryEmit(value)) {
viewModelScope.launch { viewModelScope.launch {
sharedFlow.emit(value) sharedFlow.emit(value)
} }

View File

@ -12,7 +12,6 @@ import net.newpipe.newplayer.ui.ContentScale
open class VideoPlayerViewModelDummy : VideoPlayerViewModel { open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
override var newPlayer: NewPlayer? = null override var newPlayer: NewPlayer? = null
override val internalPlayer: Player? = null
override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT)
override var minContentRatio = 4F / 3F override var minContentRatio = 4F / 3F
override var maxContentRatio = 16F / 9F override var maxContentRatio = 16F / 9F
@ -92,6 +91,18 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
println("dummy impl stream selected: $streamId") println("dummy impl stream selected: $streamId")
} }
override fun setRepeatmode(repeatMode: Int) {
println("dummy impl repeat mode: $repeatMode")
}
override fun setSuffleEnabled(enabled: Boolean) {
println("dummy impl shuffle enabled: $enabled")
}
override fun onStorePlaylist() {
TODO("Not yet implemented")
}
override fun pause() { override fun pause() {
println("dummy pause") println("dummy pause")
} }

View File

@ -53,4 +53,10 @@ suspend fun getPlaylistItemsFromItemList(items: List<String>, mediaRepo: MediaRe
} }
} }
fun getPlaylistDurationInS(items: List<PlaylistItem>) : Int {
var duration = 0
for(item in items) {
duration += item.lengthInS
}
return duration
}

View File

@ -71,7 +71,7 @@ fun VideoPlayerUI(
) { ) {
if (viewModel == null) { if (viewModel == null) {
VideoPlayerLoadingPlaceholder() VideoPlayerLoadingPlaceholder()
} else if (viewModel.internalPlayer == null) { } else if (viewModel.newPlayer == null) {
VideoPlayerLoadingPlaceholder(viewModel.uiState.collectAsState().value.embeddedUiRatio) VideoPlayerLoadingPlaceholder(viewModel.uiState.collectAsState().value.embeddedUiRatio)
} else { } else {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
@ -171,7 +171,7 @@ fun VideoPlayerUI(
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
PlaySurface( PlaySurface(
player = viewModel.internalPlayer, player = viewModel.newPlayer?.internalPlayer,
lifecycle = lifecycle, lifecycle = lifecycle,
fitMode = uiState.contentFitMode, fitMode = uiState.contentFitMode,
uiRatio = if (uiState.uiMode.fullscreen) screenRatio uiRatio = if (uiState.uiMode.fullscreen) screenRatio

View File

@ -43,6 +43,7 @@ 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 androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.viewModelScope
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.UIModeState import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.model.VideoPlayerUIState import net.newpipe.newplayer.model.VideoPlayerUIState
@ -68,7 +69,8 @@ fun BottomUI(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier modifier = modifier
) { ) {
Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: Locale.US)) val locale = getLocale()!!
Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: locale))
Seeker( Seeker(
Modifier.weight(1F), Modifier.weight(1F),
value = uiState.seekerPosition, value = uiState.seekerPosition,
@ -80,7 +82,7 @@ fun BottomUI(
//Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F)) //Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F))
Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: Locale.US)) Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: locale))
val embeddedUiConfig = getEmbeddedUiConfig(LocalContext.current as Activity) val embeddedUiConfig = getEmbeddedUiConfig(LocalContext.current as Activity)
IconButton( IconButton(

View File

@ -20,59 +20,47 @@
package net.newpipe.newplayer.ui.videoplayer package net.newpipe.newplayer.ui.videoplayer
import android.view.MotionEvent import androidx.compose.foundation.layout.Arrangement
import android.view.Surface import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.RepeatOn
import androidx.compose.material.icons.filled.RepeatOne
import androidx.compose.material.icons.filled.RepeatOneOn
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
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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.compose.ui.unit.sp import androidx.media3.exoplayer.ExoPlayer
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.VideoPlayerUIState import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR
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.getTimeStringFromMs
import coil.compose.AsyncImage
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.utils.BitmapThumbnail import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.utils.OnlineThumbnail import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem
import net.newpipe.newplayer.utils.VectorThumbnail import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar
import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamItem
import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamSelectTopBar
import net.newpipe.newplayer.utils.getInsets import net.newpipe.newplayer.utils.getInsets
@Composable @Composable
@ -93,11 +81,12 @@ fun StreamSelectUI(
containerColor = Color.Transparent, containerColor = Color.Transparent,
topBar = { topBar = {
if (isChapterSelect) { if (isChapterSelect) {
ChapterSelectTopBar(onClose = { ChapterSelectTopBar(
viewModel.closeStreamSelection() onClose =
}) viewModel::closeStreamSelection
)
} else { } else {
StreamSelectTopBar() StreamSelectTopBar(viewModel = viewModel, uiState = uiState)
} }
} }
) { innerPadding -> ) { innerPadding ->
@ -105,6 +94,8 @@ fun StreamSelectUI(
modifier = Modifier modifier = Modifier
.padding(innerPadding) .padding(innerPadding)
.fillMaxSize(), .fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp),
contentPadding = PaddingValues(start = 8.dp, end = 4.dp)
) { ) {
if (isChapterSelect) { if (isChapterSelect) {
items(uiState.chapters.size) { chapterIndex -> items(uiState.chapters.size) { chapterIndex ->
@ -120,252 +111,29 @@ fun StreamSelectUI(
) )
} }
} else { } else {
items(uiState.playList.size) { playlistItemIndex ->
} val playlistItem = uiState.playList[playlistItemIndex]
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ChapterSelectTopBar(modifier: Modifier = Modifier, onClose: () -> Unit) {
TopAppBar(modifier = modifier,
colors = topAppBarColors(containerColor = Color.Transparent),
title = {
Text("Chapter TODO")
//Text(stringResource(R.string.chapter))
}, actions = {
IconButton(
onClick = onClose
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_chapter_selection)
)
}
})
}
@Composable
private fun StreamSelectTopBar() {
}
@Composable
private fun ChapterItem(
modifier: Modifier = Modifier,
id: Int,
thumbnail: Thumbnail?,
chapterTitle: String,
chapterStartInMs: Long,
onClicked: (Int) -> Unit
) {
val locale = getLocale()!!
Row(
modifier = modifier
.padding(
start = 8.dp,
top = 4.dp,
bottom = 4.dp,
end = 4.dp
).height(80.dp)
.clickable { onClicked(id) }
) {
val contentDescription = stringResource(R.string.chapter)
if (thumbnail != null) {
when (thumbnail) {
is OnlineThumbnail -> AsyncImage(
model = thumbnail.url,
contentDescription = contentDescription
)
is BitmapThumbnail -> Image(
bitmap = thumbnail.img,
contentDescription = contentDescription
)
is VectorThumbnail -> Image(
imageVector = thumbnail.vec,
contentDescription = contentDescription
)
}
AsyncImage(
model = thumbnail,
contentDescription = contentDescription
)
} else {
Image(
painterResource(R.drawable.tiny_placeholder),
contentDescription = stringResource(R.string.chapter_thumbnail)
)
}
Column(
modifier = Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.Start,
) {
Text(text = chapterTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold)
Text(getTimeStringFromMs(chapterStartInMs, locale))
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun StreamItem(
modifier: Modifier = Modifier,
id: Int,
title: String,
creator: String?,
thumbnail: Thumbnail?,
lengthInMs: Long,
onDragStart: (Int) -> Unit,
onDragEnd: (Int) -> Unit,
onClicked: (Int) -> Unit
) {
val locale = getLocale()!!
Row(modifier = modifier.clickable { onClicked(id) }) {
Box {
val contentDescription = stringResource(R.string.chapter)
if (thumbnail != null) {
when (thumbnail) {
is OnlineThumbnail -> AsyncImage(
model = thumbnail.url,
contentDescription = contentDescription
)
is BitmapThumbnail -> Image(
bitmap = thumbnail.img,
contentDescription = contentDescription
)
is VectorThumbnail -> Image(
imageVector = thumbnail.vec,
contentDescription = contentDescription
)
}
AsyncImage(
model = thumbnail,
contentDescription = contentDescription
)
} else {
Image(
painterResource(R.drawable.tiny_placeholder),
contentDescription = stringResource(R.string.chapter_thumbnail)
)
}
Surface(
color = CONTROLLER_UI_BACKGROUND_COLOR,
modifier = Modifier
.wrapContentSize()
.align(Alignment.BottomEnd)
.padding(4.dp)
) {
Text(
modifier = Modifier.padding(
start = 4.dp,
end = 4.dp,
top = 2.dp,
bottom = 2.dp
), text = getTimeStringFromMs(lengthInMs, locale)
)
}
}
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
.fillMaxSize()
) {
Text(text = title, fontSize = 18.sp, fontWeight = FontWeight.Bold)
if (creator != null) {
Text(text = creator)
}
}
Box(modifier = Modifier
.fillMaxHeight()
.aspectRatio(1f)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_UP -> {
onDragEnd(id)
false
}
MotionEvent.ACTION_DOWN -> {
onDragStart(id)
false
}
else -> true
}
}) {
Icon(
modifier = Modifier
.size(40.dp)
.align(Alignment.Center),
imageVector = Icons.Filled.DragHandle,
//contentDescription = stringResource(R.string.stream_item_drag_handle)
contentDescription = "placeholer, TODO: FIXME"
)
}
}
}
@Preview(device = "spec:width=1080px,height=300px,dpi=440,orientation=landscape")
@Composable
fun ChapterItemPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
ChapterItem(
id = 0,
thumbnail = null,
modifier = Modifier.fillMaxSize(),
chapterTitle = "Chapter Title",
chapterStartInMs = (4 * 60 + 32) * 1000,
onClicked = {}
)
}
}
}
@Preview(device = "spec:width=1080px,height=200px,dpi=440,orientation=landscape")
@Composable
fun StreamItemPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
StreamItem( StreamItem(
id = 0, id = playlistItemIndex,
modifier = Modifier.fillMaxSize(), title = playlistItem.title,
title = "Video Title", creator = playlistItem.creator,
creator = "Video Creator", thumbnail = playlistItem.thumbnail,
thumbnail = null, lengthInMs = playlistItem.lengthInS.toLong() * 1000,
lengthInMs = 15 * 60 * 1000,
onDragStart = {}, onDragStart = {},
onDragEnd = {}, onDragEnd = {},
onClicked = {} onClicked = { viewModel.streamSelected(playlistItemIndex) }
) )
} }
} }
}
}
}
} }
@Preview(device = "spec:width=1080px,height=150px,dpi=440,orientation=landscape")
@Composable
fun ChapterTopBarPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
ChapterSelectTopBar(modifier = Modifier.fillMaxSize()) {}
}
}
}
@Preview(device = "id:pixel_5") @Preview(device = "id:pixel_5")
@Composable @Composable
fun VideoPlayerStreamSelectUIPreview() { fun VideoPlayerChannelSelectUIPreview() {
VideoPlayerTheme { VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.Red) { Surface(modifier = Modifier.fillMaxSize(), color = Color.Red) {
StreamSelectUI( StreamSelectUI(
@ -394,3 +162,41 @@ fun VideoPlayerStreamSelectUIPreview() {
} }
} }
} }
@Preview(device = "id:pixel_5")
@Composable
fun VideoPlayerStreamSelectUIPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.Red) {
StreamSelectUI(
isChapterSelect = false,
viewModel = VideoPlayerViewModelDummy(),
uiState = VideoPlayerUIState.DEFAULT.copy(
playList = arrayListOf(
PlaylistItem(
id = "6502",
title = "Stream 1",
creator = "The Creator",
lengthInS = 6 * 60 + 5,
thumbnail = null
),
PlaylistItem(
id = "6502",
title = "Stream 2",
creator = "The Creator 2",
lengthInS = 2 * 60 + 5,
thumbnail = null
),
PlaylistItem(
id = "6502",
title = "Stream 3",
creator = "The Creator 3",
lengthInS = 29 * 60 + 5,
thumbnail = null
)
)
)
)
}
}
}

View File

@ -0,0 +1,123 @@
/* 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.videoplayer.streamselect
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.BitmapThumbnail
import net.newpipe.newplayer.utils.OnlineThumbnail
import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.VectorThumbnail
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
@Composable
fun ChapterItem(
modifier: Modifier = Modifier,
id: Int,
thumbnail: Thumbnail?,
chapterTitle: String,
chapterStartInMs: Long,
onClicked: (Int) -> Unit
) {
val locale = getLocale()!!
Row(
modifier = modifier
.height(80.dp)
.clickable { onClicked(id) }
) {
val contentDescription = stringResource(R.string.chapter)
if (thumbnail != null) {
when (thumbnail) {
is OnlineThumbnail -> AsyncImage(
model = thumbnail.url,
contentDescription = contentDescription
)
is BitmapThumbnail -> Image(
bitmap = thumbnail.img,
contentDescription = contentDescription
)
is VectorThumbnail -> Image(
imageVector = thumbnail.vec,
contentDescription = contentDescription
)
}
AsyncImage(
model = thumbnail,
contentDescription = contentDescription
)
} else {
Image(
painterResource(R.drawable.tiny_placeholder),
contentDescription = stringResource(R.string.chapter_thumbnail)
)
}
Column(
modifier = Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.Start,
) {
Text(text = chapterTitle, fontSize = 18.sp, fontWeight = FontWeight.Bold)
Text(getTimeStringFromMs(chapterStartInMs, locale))
}
}
}
@Preview(device = "spec:width=1080px,height=300px,dpi=440,orientation=landscape")
@Composable
fun ChapterItemPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
ChapterItem(
id = 0,
thumbnail = null,
modifier = Modifier.fillMaxSize(),
chapterTitle = "Chapter Title",
chapterStartInMs = (4 * 60 + 32) * 1000,
onClicked = {}
)
}
}
}

View File

@ -0,0 +1,70 @@
/* 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.videoplayer.streamselect
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChapterSelectTopBar(modifier: Modifier = Modifier, onClose: () -> Unit) {
TopAppBar(modifier = modifier,
colors = topAppBarColors(containerColor = Color.Transparent),
title = {
Text("Chapter TODO")
//Text(stringResource(R.string.chapter))
}, actions = {
IconButton(
onClick = onClose
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_chapter_selection)
)
}
})
}
@Preview(device = "spec:width=1080px,height=150px,dpi=440,orientation=landscape")
@Composable
fun ChapterTopBarPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
ChapterSelectTopBar(modifier = Modifier.fillMaxSize()) {}
}
}
}

View File

@ -0,0 +1,188 @@
/* 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.videoplayer.streamselect
import android.view.MotionEvent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.BitmapThumbnail
import net.newpipe.newplayer.utils.OnlineThumbnail
import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.VectorThumbnail
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun StreamItem(
modifier: Modifier = Modifier,
id: Int,
title: String,
creator: String?,
thumbnail: Thumbnail?,
lengthInMs: Long,
onDragStart: (Int) -> Unit,
onDragEnd: (Int) -> Unit,
onClicked: (Int) -> Unit
) {
val locale = getLocale()!!
Row(modifier = modifier
.clickable { onClicked(id) }
.height(80.dp)) {
Box {
val contentDescription = stringResource(R.string.chapter)
if (thumbnail != null) {
when (thumbnail) {
is OnlineThumbnail -> AsyncImage(
model = thumbnail.url,
contentDescription = contentDescription
)
is BitmapThumbnail -> Image(
bitmap = thumbnail.img,
contentDescription = contentDescription
)
is VectorThumbnail -> Image(
imageVector = thumbnail.vec,
contentDescription = contentDescription
)
}
AsyncImage(
model = thumbnail,
contentDescription = contentDescription
)
} else {
Image(
painterResource(R.drawable.tiny_placeholder),
contentDescription = stringResource(R.string.chapter_thumbnail)
)
}
Surface(
color = CONTROLLER_UI_BACKGROUND_COLOR,
modifier = Modifier
.wrapContentSize()
.align(Alignment.BottomEnd)
.padding(4.dp)
) {
Text(
modifier = Modifier.padding(
start = 4.dp,
end = 4.dp,
top = 2.dp,
bottom = 2.dp
), text = getTimeStringFromMs(lengthInMs, locale)
)
}
}
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
.fillMaxSize()
) {
Text(text = title, fontSize = 18.sp, fontWeight = FontWeight.Bold)
if (creator != null) {
Text(text = creator)
}
}
Box(modifier = Modifier
.fillMaxHeight()
.aspectRatio(1f)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_UP -> {
onDragEnd(id)
false
}
MotionEvent.ACTION_DOWN -> {
onDragStart(id)
false
}
else -> true
}
}) {
Icon(
modifier = Modifier
.size(40.dp)
.align(Alignment.Center),
imageVector = Icons.Filled.DragHandle,
//contentDescription = stringResource(R.string.stream_item_drag_handle)
contentDescription = "placeholer, TODO: FIXME"
)
}
}
}
@Preview(device = "spec:width=1080px,height=200px,dpi=440,orientation=landscape")
@Composable
fun StreamItemPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
StreamItem(
id = 0,
modifier = Modifier.fillMaxSize(),
title = "Video Title",
creator = "Video Creator",
thumbnail = null,
lengthInMs = 15 * 60 * 1000,
onDragStart = {},
onDragEnd = {},
onClicked = {}
)
}
}
}

View File

@ -0,0 +1,157 @@
/* 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.videoplayer.streamselect
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.PlaylistAdd
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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.media3.common.Player
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.playerInternals.getPlaylistDurationInS
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StreamSelectTopBar(
modifier: Modifier = Modifier,
viewModel: VideoPlayerViewModel,
uiState: VideoPlayerUIState
) {
TopAppBar(modifier = modifier,
colors = topAppBarColors(containerColor = Color.Transparent),
title = {
val locale = getLocale()!!
val duration = getPlaylistDurationInS(uiState.playList).toLong() * 1000
val durationString = getTimeStringFromMs(timeSpanInMs = duration, locale)
Text(
text = "00:00/$durationString"
)
}, actions = {
IconButton(
onClick = {
viewModel.setRepeatmode(
when (uiState.repeatMode) {
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL
Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_OFF
else -> throw NewPlayerException("Unknown repeat mode: ${uiState.repeatMode}")
}
)
}
) {
when (uiState.repeatMode) {
Player.REPEAT_MODE_OFF -> Icon(
imageVector = Icons.Filled.Repeat,
contentDescription = stringResource(R.string.repeat_mode_no_repeat)
)
Player.REPEAT_MODE_ALL -> Icon(
imageVector = Icons.Filled.RepeatOn,
contentDescription = stringResource(R.string.repeat_mode_repeat_all)
)
Player.REPEAT_MODE_ONE -> Icon(
imageVector = Icons.Filled.RepeatOneOn,
contentDescription = stringResource(R.string.repeat_mode_repeat_all)
)
else -> throw NewPlayerException("Unknown repeat mode: ${uiState.repeatMode}")
}
}
IconButton(
onClick = {
viewModel.setSuffleEnabled(!uiState.shuffleEnabled)
}
) {
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(
onClick = viewModel::onStorePlaylist
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
contentDescription = stringResource(R.string.store_playlist)
)
}
IconButton(
onClick = viewModel::closeStreamSelection
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_stream_selection)
)
}
})
}
@Preview(device = "spec:width=1080px,height=150px,dpi=440,orientation=landscape")
@Composable
fun StreamSelectTopBarPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
StreamSelectTopBar(
modifier = Modifier.fillMaxSize(),
viewModel = VideoPlayerViewModelDummy(),
uiState = VideoPlayerUIState.DEFAULT
)
}
}
}

View File

@ -126,7 +126,7 @@ fun getTimeStringFromMs(timeSpanInMs: Long, locale: Locale): String {
val time_string = val time_string =
if (0L < days) String.format(locale, "%d:%02d:%02d:%02d", days, hours, minutes, seconds) if (0L < days) String.format(locale, "%d:%02d:%02d:%02d", days, hours, minutes, seconds)
else if (0L < hours) String.format(locale, "%d:%02d:%02d", hours, minutes, seconds) else if (0L < hours) String.format(locale, "%d:%02d:%02d", hours, minutes, seconds)
else String.format(locale, "%d:%02d", minutes, seconds) else String.format(locale, "%02d:%02d", minutes, seconds)
return time_string return time_string
} }

View File

@ -45,4 +45,10 @@
<string name="chapter">Chapter</string> <string name="chapter">Chapter</string>
<string name="chapter_thumbnail">Chapter Thumbnail</string> <string name="chapter_thumbnail">Chapter Thumbnail</string>
<string name="stream_item_drag_handle">Stream item drag handle</string> <string name="stream_item_drag_handle">Stream item drag handle</string>
<string name="repeat_mode_no_repeat">Repeat mode: No repeat</string>
<string name="repeat_mode_repeat_all">Repeat mode: Repeat all</string>
<string name="repeat_mode_repeat_current">Repeat mode: Repeat currently playing</string>
<string name="shuffle_on">Shuffle playlist enabled</string>
<string name="shuffle_off">Shuffle playlist disabled</string>
<string name="store_playlist">Save current playlist</string>
</resources> </resources>