diff --git a/.kotlin/errors/errors-1725028164833.log b/.kotlin/errors/errors-1725028164833.log new file mode 100644 index 0000000..9300f96 --- /dev/null +++ b/.kotlin/errors/errors-1725028164833.log @@ -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 + diff --git a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt index d4bf405..429a0c4 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt @@ -23,8 +23,10 @@ package net.newpipe.newplayer import android.app.Application import android.util.Log import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException import androidx.media3.common.Player +import androidx.media3.common.Timeline import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.MediaSource import kotlinx.coroutines.CoroutineScope @@ -39,6 +41,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.newpipe.newplayer.playerInternals.PlayList +import net.newpipe.newplayer.playerInternals.PlaylistItem +import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromItemList import kotlin.Exception enum class PlayMode { @@ -59,14 +63,14 @@ interface NewPlayer { var playWhenReady: Boolean val duration: Long val bufferedPercentage: Int - val repository: MediaRepository val sharingLinkWithOffsetPossible: Boolean var currentPosition: Long var fastSeekAmountSec: Int var playBackMode: PlayMode var playMode: StateFlow - var playlist: PlayList + val playlist: PlayList + val playlistInPlaylistItems: StateFlow> // callbacks @@ -87,17 +91,17 @@ interface NewPlayer { private var preferredStreamVariants: List = emptyList() private var sharingLinkWithOffsetPossible = false - fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory) : Builder { + fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory): Builder { this.mediaSourceFactory = mediaSourceFactory return this } - fun setPreferredStreamVariants(preferredStreamVariants: List) : Builder { + fun setPreferredStreamVariants(preferredStreamVariants: List): Builder { this.preferredStreamVariants = preferredStreamVariants return this } - fun setSharingLinkWithOffsetPossible(possible: Boolean) : Builder { + fun setSharingLinkWithOffsetPossible(possible: Boolean): Builder { this.sharingLinkWithOffsetPossible = false return this } @@ -123,7 +127,7 @@ class NewPlayerImpl( val app: Application, override val internalPlayer: Player, override val preferredStreamVariants: List, - override val repository: MediaRepository, + private val repository: MediaRepository, override val sharingLinkWithOffsetPossible: Boolean ) : NewPlayer { @@ -161,9 +165,14 @@ class NewPlayerImpl( override val duration: Long get() = internalPlayer.duration - override var playlist = PlayList(internalPlayer) + override val playlist = PlayList(internalPlayer) + + val mutablePlaylistAsPlaylistItems = MutableStateFlow>(emptyList()) + override val playlistInPlaylistItems: StateFlow> = + mutablePlaylistAsPlaylistItems.asStateFlow() init { + println("gurken init") internalPlayer.addListener(object : Player.Listener { override fun onPlayerError(error: PlaybackException) { launchJobAndCollectError { @@ -183,9 +192,28 @@ class NewPlayerImpl( 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() { internalPlayer.prepare() } diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt index 7dfc91c..07c8f85 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt @@ -20,8 +20,7 @@ package net.newpipe.newplayer.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import androidx.media3.common.Player import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.playerInternals.PlaylistItem import net.newpipe.newplayer.ui.ContentScale @@ -42,7 +41,10 @@ data class VideoPlayerUIState( val brightness: Float?, // when null use system value val embeddedUiConfig: EmbeddedUiConfig?, val playList: List, - val chapters: List + val chapters: List, + val shuffleEnabled: Boolean, + val repeatMode: Int, + val playListDurationInS: Int ) { companion object { val DEFAULT = VideoPlayerUIState( @@ -64,7 +66,10 @@ data class VideoPlayerUIState( brightness = null, embeddedUiConfig = null, playList = emptyList(), - chapters = emptyList() + chapters = emptyList(), + shuffleEnabled = false, + repeatMode = Player.REPEAT_MODE_OFF, + playListDurationInS = 0 ) } } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt index 6625bc1..414e9e6 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt @@ -32,7 +32,6 @@ import net.newpipe.newplayer.utils.Thumbnail interface VideoPlayerViewModel { var newPlayer: NewPlayer? - val internalPlayer: Player? val uiState: StateFlow var minContentRatio: Float var maxContentRatio: Float @@ -61,4 +60,7 @@ interface VideoPlayerViewModel { fun closeStreamSelection() fun chapterSelected(chapter: Chapter) fun streamSelected(streamId: Int) + fun setRepeatmode(repeatMode: Int) + fun setSuffleEnabled(enabled: Boolean) + fun onStorePlaylist() } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt index e1927fe..3eb3fa8 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt @@ -90,9 +90,6 @@ class VideoPlayerViewModelImpl @Inject constructor( override val uiState = mutableUiState.asStateFlow() - override val internalPlayer: Player? - get() = newPlayer?.internalPlayer - override var minContentRatio: Float = 4F / 3F set(value) { if (value <= 0 || maxContentRatio < value) Log.e( @@ -133,7 +130,7 @@ class VideoPlayerViewModelImpl @Inject constructor( override val onBackPressed: SharedFlow = mutableOnBackPressed.asSharedFlow() private fun installNewPlayer() { - internalPlayer?.let { player -> + newPlayer?.internalPlayer?.let { player -> Log.d(TAG, "Install player: ${player.videoSize.width}") player.addListener(object : Player.Listener { @@ -158,11 +155,6 @@ class VideoPlayerViewModelImpl @Inject constructor( it.copy(isLoading = isLoading) } } - - override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) { - super.onPlaylistMetadataChanged(mediaMetadata) - updatePlaylist() - } }) } 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) { @@ -266,9 +262,10 @@ class VideoPlayerViewModelImpl @Inject constructor( } private fun updateProgressOnce() { - val progress = internalPlayer?.currentPosition ?: 0 - val duration = internalPlayer?.duration ?: 1 - val bufferedPercentage = (internalPlayer?.bufferedPercentage?.toFloat() ?: 0f) / 100f + val progress = newPlayer?.currentPosition ?: 0 + val duration = newPlayer?.duration ?: 1 + val bufferedPercentage = + (newPlayer?.bufferedPercentage?.toFloat() ?: 0f) / 100f val progressPercentage = progress.toFloat() / duration.toFloat() mutableUiState.update { @@ -297,13 +294,13 @@ class VideoPlayerViewModelImpl @Inject constructor( override fun seekingFinished() { resetHideUiDelayedJob() val seekerPosition = mutableUiState.value.seekerPosition - val seekPositionInMs = (internalPlayer?.duration?.toFloat() ?: 0F) * seekerPosition + val seekPositionInMs = (newPlayer?.duration?.toFloat() ?: 0F) * seekerPosition newPlayer?.currentPosition = seekPositionInMs.toLong() Log.i(TAG, "Seek to Ms: $seekPositionInMs") } override fun embeddedDraggedDown(offset: Float) { - saveTryEmit(mutableEmbeddedPlayerDraggedDownBy, offset) + safeTryEmit(mutableEmbeddedPlayerDraggedDownBy, offset) } override fun fastSeek(count: Int) { @@ -362,7 +359,6 @@ class VideoPlayerViewModelImpl @Inject constructor( } override fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig) { - println("gurken openSelection ${embeddedUiConfig}") uiVisibilityJob?.cancel() if (!uiState.value.uiMode.fullscreen) { this.embeddedUiConfig = embeddedUiConfig @@ -388,7 +384,7 @@ class VideoPlayerViewModelImpl @Inject constructor( if (nextMode != null) { updateUiMode(nextMode) } else { - saveTryEmit(mutableOnBackPressed, Unit) + safeTryEmit(mutableOnBackPressed, Unit) } } @@ -408,6 +404,25 @@ class VideoPlayerViewModelImpl @Inject constructor( 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) { val newPlayMode = newState.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() return (if (videoRatio.isNaN()) currentContentRatio else videoRatio).coerceIn(minContentRatio, maxContentRatio) @@ -428,21 +443,8 @@ class VideoPlayerViewModelImpl @Inject constructor( } ?: minContentRatio - private fun updatePlaylist() { - newPlayer?.let { newPlayer -> - viewModelScope.launch { - val playlist = getPlaylistItemsFromItemList( - newPlayer.playlist, newPlayer.repository - ) - mutableUiState.update { - it.copy(playList = playlist) - } - } - } - } - - private fun saveTryEmit(sharedFlow: MutableSharedFlow, value: T) { - if(sharedFlow.tryEmit(value)) { + private fun safeTryEmit(sharedFlow: MutableSharedFlow, value: T) { + if (!sharedFlow.tryEmit(value)) { viewModelScope.launch { sharedFlow.emit(value) } diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt b/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt index 2acc865..c9b5f2b 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt @@ -12,7 +12,6 @@ import net.newpipe.newplayer.ui.ContentScale open class VideoPlayerViewModelDummy : VideoPlayerViewModel { override var newPlayer: NewPlayer? = null - override val internalPlayer: Player? = null override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) override var minContentRatio = 4F / 3F override var maxContentRatio = 16F / 9F @@ -92,6 +91,18 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel { 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() { println("dummy pause") } diff --git a/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayListItem.kt b/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayListItem.kt index 5c97d67..bb82777 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayListItem.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayListItem.kt @@ -53,4 +53,10 @@ suspend fun getPlaylistItemsFromItemList(items: List, mediaRepo: MediaRe } } - +fun getPlaylistDurationInS(items: List) : Int { + var duration = 0 + for(item in items) { + duration += item.lengthInS + } + return duration +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt index 2ca1123..bd16870 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt @@ -71,7 +71,7 @@ fun VideoPlayerUI( ) { if (viewModel == null) { VideoPlayerLoadingPlaceholder() - } else if (viewModel.internalPlayer == null) { + } else if (viewModel.newPlayer == null) { VideoPlayerLoadingPlaceholder(viewModel.uiState.collectAsState().value.embeddedUiRatio) } else { val uiState by viewModel.uiState.collectAsState() @@ -171,7 +171,7 @@ fun VideoPlayerUI( ) { Box(contentAlignment = Alignment.Center) { PlaySurface( - player = viewModel.internalPlayer, + player = viewModel.newPlayer?.internalPlayer, lifecycle = lifecycle, fitMode = uiState.contentFitMode, uiRatio = if (uiState.uiMode.fullscreen) screenRatio diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/BottomUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/BottomUI.kt index b4a1561..1623f45 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/BottomUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/BottomUI.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.core.os.ConfigurationCompat +import androidx.lifecycle.viewModelScope import net.newpipe.newplayer.R import net.newpipe.newplayer.model.UIModeState import net.newpipe.newplayer.model.VideoPlayerUIState @@ -68,7 +69,8 @@ fun BottomUI( horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier ) { - Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: Locale.US)) + val locale = getLocale()!! + Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: locale)) Seeker( Modifier.weight(1F), value = uiState.seekerPosition, @@ -80,7 +82,7 @@ fun BottomUI( //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) IconButton( diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt index 4d611f2..74b4a56 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt @@ -20,59 +20,47 @@ package net.newpipe.newplayer.ui.videoplayer -import android.view.MotionEvent -import android.view.Surface -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.Arrangement +import androidx.compose.foundation.layout.PaddingValues 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.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons 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.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Scaffold 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.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 androidx.media3.exoplayer.ExoPlayer 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.ui.CONTROLLER_UI_BACKGROUND_COLOR 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.utils.BitmapThumbnail -import net.newpipe.newplayer.utils.OnlineThumbnail -import net.newpipe.newplayer.utils.Thumbnail -import net.newpipe.newplayer.utils.VectorThumbnail +import net.newpipe.newplayer.NewPlayerException +import net.newpipe.newplayer.playerInternals.PlaylistItem +import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem +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 @Composable @@ -93,11 +81,12 @@ fun StreamSelectUI( containerColor = Color.Transparent, topBar = { if (isChapterSelect) { - ChapterSelectTopBar(onClose = { - viewModel.closeStreamSelection() - }) + ChapterSelectTopBar( + onClose = + viewModel::closeStreamSelection + ) } else { - StreamSelectTopBar() + StreamSelectTopBar(viewModel = viewModel, uiState = uiState) } } ) { innerPadding -> @@ -105,6 +94,8 @@ fun StreamSelectUI( modifier = Modifier .padding(innerPadding) .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(start = 8.dp, end = 4.dp) ) { if (isChapterSelect) { items(uiState.chapters.size) { chapterIndex -> @@ -120,252 +111,29 @@ fun StreamSelectUI( ) } } else { - - } - } - } - } -} - -@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 + items(uiState.playList.size) { playlistItemIndex -> + val playlistItem = uiState.playList[playlistItemIndex] + StreamItem( + id = playlistItemIndex, + title = playlistItem.title, + creator = playlistItem.creator, + thumbnail = playlistItem.thumbnail, + lengthInMs = playlistItem.lengthInS.toLong() * 1000, + onDragStart = {}, + onDragEnd = {}, + onClicked = { viewModel.streamSelected(playlistItemIndex) } + ) } - - 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( - id = 0, - modifier = Modifier.fillMaxSize(), - title = "Video Title", - creator = "Video Creator", - thumbnail = null, - lengthInMs = 15 * 60 * 1000, - onDragStart = {}, - onDragEnd = {}, - onClicked = {} - ) - } - } -} - -@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") @Composable -fun VideoPlayerStreamSelectUIPreview() { +fun VideoPlayerChannelSelectUIPreview() { VideoPlayerTheme { Surface(modifier = Modifier.fillMaxSize(), color = Color.Red) { StreamSelectUI( @@ -393,4 +161,42 @@ 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 + ) + ) + ) + ) + } + } } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterItem.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterItem.kt new file mode 100644 index 0000000..b11c52d --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterItem.kt @@ -0,0 +1,123 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + + +package net.newpipe.newplayer.ui.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 = {} + ) + } + } +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterSelectTopBar.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterSelectTopBar.kt new file mode 100644 index 0000000..5aa0b6d --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterSelectTopBar.kt @@ -0,0 +1,70 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + + +package net.newpipe.newplayer.ui.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()) {} + } + } +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt new file mode 100644 index 0000000..8759d62 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt @@ -0,0 +1,188 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + +package net.newpipe.newplayer.ui.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 = {} + ) + } + } +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamSelectTopBar.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamSelectTopBar.kt new file mode 100644 index 0000000..cc41294 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamSelectTopBar.kt @@ -0,0 +1,157 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + +package net.newpipe.newplayer.ui.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 + ) + } + } +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt b/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt index 90cfc3e..28b8a01 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt @@ -126,7 +126,7 @@ fun getTimeStringFromMs(timeSpanInMs: Long, locale: Locale): String { val time_string = 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 String.format(locale, "%d:%02d", minutes, seconds) + else String.format(locale, "%02d:%02d", minutes, seconds) return time_string } diff --git a/new-player/src/main/res/values/strings.xml b/new-player/src/main/res/values/strings.xml index 84754bf..d7df845 100644 --- a/new-player/src/main/res/values/strings.xml +++ b/new-player/src/main/res/values/strings.xml @@ -45,4 +45,10 @@ Chapter Chapter Thumbnail Stream item drag handle + Repeat mode: No repeat + Repeat mode: Repeat all + Repeat mode: Repeat currently playing + Shuffle playlist enabled + Shuffle playlist disabled + Save current playlist \ No newline at end of file