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 new file mode 100644 index 0000000..86ae2ae --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt @@ -0,0 +1,36 @@ +package net.newpipe.newplayer.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import net.newpipe.newplayer.ui.ContentScale + +@Parcelize +data class VideoPlayerUIState( + val playing: Boolean, + var fullscreen: Boolean, + val uiVissible: Boolean, + var uiVisible: Boolean, + val contentRatio: Float, + val embeddedUiRatio: Float, + val contentFitMode: ContentScale, + val seekerPosition: Float, + val isLoading: Boolean, + val durationInMs: Long, + val playbackPositionInMs: Long +) : Parcelable { + companion object { + val DEFAULT = VideoPlayerUIState( + playing = false, + fullscreen = false, + uiVissible = false, + uiVisible = false, + contentRatio = 16 / 9F, + embeddedUiRatio = 16F / 9F, + contentFitMode = ContentScale.FIT_INSIDE, + seekerPosition = 0F, + isLoading = true, + durationInMs = 0, + playbackPositionInMs = 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 6324449..582a9d3 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 @@ -1,79 +1,11 @@ -/* 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.model -import android.app.Application -import android.os.Build import android.os.Bundle -import android.os.Parcelable -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope import androidx.media3.common.Player -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import net.newpipe.newplayer.utils.VideoSize -import kotlinx.parcelize.Parcelize import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.ui.ContentScale -val VIDEOPLAYER_UI_STATE = "video_player_ui_state" - -private const val TAG = "VideoPlayerViewModel" - -@Parcelize -data class VideoPlayerUIState( - val playing: Boolean, - var fullscreen: Boolean, - val uiVissible: Boolean, - var uiVisible: Boolean, - val contentRatio: Float, - val embeddedUiRatio: Float, - val contentFitMode: ContentScale, - val seekerPosition: Float, - val isLoading: Boolean -) : Parcelable { - companion object { - val DEFAULT = VideoPlayerUIState( - playing = false, - fullscreen = false, - uiVissible = false, - uiVisible = false, - contentRatio = 16 / 9F, - embeddedUiRatio = 16F / 9F, - contentFitMode = ContentScale.FIT_INSIDE, - seekerPosition = 0F, - isLoading = true - ) - } -} interface VideoPlayerViewModel { var newPlayer: NewPlayer? @@ -100,301 +32,4 @@ interface VideoPlayerViewModel { fun onFullscreenToggle(isFullscreen: Boolean) fun onUiVissibleToggle(isVissible: Boolean) } -} - -@HiltViewModel -class VideoPlayerViewModelImpl @Inject constructor( - private val savedStateHandle: SavedStateHandle, - application: Application, -) : AndroidViewModel(application), VideoPlayerViewModel { - - // private - private val mutableUiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) - private var currentContentRatio = 1F - - private var uiVisibilityJob: Job? = null - private var progressUpdaterJob: Job? = null - - //interface - override var callbackListener: VideoPlayerViewModel.Listener? = null - - override var newPlayer: NewPlayer? = null - set(value) { - field = value - installExoPlayer() - } - - override val uiState = mutableUiState.asStateFlow() - - override val player: Player? - get() = newPlayer?.player - - override var minContentRatio: Float = 4F / 3F - set(value) { - if (value <= 0 || maxContentRatio < value) - Log.e( - TAG, - "Ignoring maxContentRatio: It must not be 0 or less and it may not be bigger then mmaxContentRatio. It was Set to: $value" - ) - else { - field = value - mutableUiState.update { it.copy(embeddedUiRatio = getEmbeddedUiRatio()) } - } - } - - - override var maxContentRatio: Float = 16F / 9F - set(value) { - if (value <= 0 || value < minContentRatio) - Log.e( - TAG, - "Ignoring maxContentRatio: It must not be 0 or less and it may not be smaller then minContentRatio. It was Set to: $value" - ) - else { - field = value - mutableUiState.update { it.copy(embeddedUiRatio = getEmbeddedUiRatio()) } - } - } - - override var contentFitMode: ContentScale - get() = mutableUiState.value.contentFitMode - set(value) { - mutableUiState.update { - it.copy(contentFitMode = value) - } - } - - private fun installExoPlayer() { - player?.let { player -> - Log.d(TAG, "Install player: ${player.videoSize.width}") - - player.addListener(object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - Log.d(TAG, "Playing state changed. Is Playing: $isPlaying") - mutableUiState.update { - it.copy(playing = isPlaying) - } - } - - override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) { - super.onVideoSizeChanged(videoSize) - updateContentRatio(VideoSize.fromMedia3VideoSize(videoSize)) - } - - override fun onIsLoadingChanged(isLoading: Boolean) { - super.onIsLoadingChanged(isLoading) - mutableUiState.update { - it.copy(isLoading = isLoading) - } - Log.i(TAG, if (isLoading) "Player started loading" else "Player finished loading") - } - }) - } - } - - fun updateContentRatio(videoSize: VideoSize) { - val newRatio = videoSize.getRatio() - val ratio = if (newRatio.isNaN()) currentContentRatio else newRatio - currentContentRatio = ratio - Log.d(TAG, "Update Content ratio: $ratio") - mutableUiState.update { - it.copy( - contentRatio = currentContentRatio, - embeddedUiRatio = getEmbeddedUiRatio() - ) - } - } - - override fun onCleared() { - super.onCleared() - - Log.d(TAG, "viewmodel cleared") - } - - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - override fun initUIState(instanceState: Bundle) { - - val uiState = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) instanceState.getParcelable( - VIDEOPLAYER_UI_STATE, VideoPlayerUIState::class.java - ) - else instanceState.getParcelable(VIDEOPLAYER_UI_STATE) - - uiState?.let { uiState -> - mutableUiState.update { - uiState - } - } - } - - override fun play() { - hideUi() - newPlayer?.play() - } - - override fun pause() { - uiVisibilityJob?.cancel() - newPlayer?.pause() - - } - - override fun prevStream() { - resetHideUiDelayedJob() - Log.e(TAG, "imeplement prev stream") - } - - override fun nextStream() { - resetHideUiDelayedJob() - Log.e(TAG, "implement next stream") - } - - override fun showUi() { - if (mutableUiState.value.fullscreen) - callbackListener?.onUiVissibleToggle(true) - - mutableUiState.update { - it.copy(uiVissible = true) - } - resetHideUiDelayedJob() - resetProgressUpdatePeriodicallyJob() - } - - private fun resetHideUiDelayedJob() { - uiVisibilityJob?.cancel() - uiVisibilityJob = viewModelScope.launch { - delay(4000) - hideUi() - } - } - - private fun resetProgressUpdatePeriodicallyJob() { - progressUpdaterJob?.cancel() - progressUpdaterJob = viewModelScope.launch { - while(true) { - updateProgressOnce() - delay(1000) - } - } - - } - - private fun updateProgressOnce() { - val progress = player?.currentPosition ?: 0 - val duration = player?.duration ?: 1 - val progressPercentage = progress.toFloat() / duration.toFloat() - mutableUiState.update { - it.copy(seekerPosition = progressPercentage) - } - } - - override fun hideUi() { - if (mutableUiState.value.fullscreen) - callbackListener?.onUiVissibleToggle(false) - - progressUpdaterJob?.cancel() - uiVisibilityJob?.cancel() - mutableUiState.update { - it.copy(uiVissible = false) - } - } - - override fun seekPositionChanged(newValue: Float) { - uiVisibilityJob?.cancel() - mutableUiState.update { it.copy(seekerPosition = newValue) } - } - - override fun seekingFinished() { - resetHideUiDelayedJob() - val seekerPosition = mutableUiState.value.seekerPosition - val seekPositionInMs = (player?.duration?.toFloat() ?: 0F) * seekerPosition - player?.seekTo(seekPositionInMs.toLong()) - Log.i(TAG, "Seek to Ms: $seekPositionInMs") - } - - override fun switchToEmbeddedView() { - callbackListener?.onFullscreenToggle(false) - uiVisibilityJob?.cancel() - mutableUiState.update { - it.copy(fullscreen = false, uiVissible = false) - } - } - - override fun switchToFullscreen() { - callbackListener?.onFullscreenToggle(true) - uiVisibilityJob?.cancel() - - mutableUiState.update { - it.copy(fullscreen = true, uiVissible = false) - } - } - - private fun getEmbeddedUiRatio() = - player?.let { player -> - val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio() - return (if (videoRatio.isNaN()) - currentContentRatio - else - videoRatio).coerceIn(minContentRatio, maxContentRatio) - - - } ?: minContentRatio - - - companion object { - val dummy = object : VideoPlayerViewModel { - override var newPlayer: NewPlayer? = null - override val player: Player? = null - override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) - override var minContentRatio = 4F / 3F - override var maxContentRatio = 16F / 9F - override var contentFitMode = ContentScale.FIT_INSIDE - override var callbackListener: VideoPlayerViewModel.Listener? = null - - override fun initUIState(instanceState: Bundle) { - println("dummy impl") - } - - - override fun play() { - println("dummy impl") - } - - override fun switchToEmbeddedView() { - println("dummy impl") - } - - override fun switchToFullscreen() { - println("dummy impl") - } - - override fun showUi() { - println("dummy impl") - } - - override fun hideUi() { - println("dummy impl") - } - - override fun seekPositionChanged(newValue: Float) { - println("dummy impl") - } - - override fun seekingFinished() { - println("dummy impl") - } - - override fun pause() { - println("dummy pause") - } - - override fun prevStream() { - println("dummy impl") - } - - override fun nextStream() { - println("dummy impl") - } - } - } } \ 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 new file mode 100644 index 0000000..661da78 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt @@ -0,0 +1,350 @@ +/* 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.model + +import android.app.Application +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.media3.common.Player +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import net.newpipe.newplayer.utils.VideoSize +import net.newpipe.newplayer.NewPlayer +import net.newpipe.newplayer.ui.ContentScale + +val VIDEOPLAYER_UI_STATE = "video_player_ui_state" + +private const val TAG = "VideoPlayerViewModel" + +@HiltViewModel +class VideoPlayerViewModelImpl @Inject constructor( + private val savedStateHandle: SavedStateHandle, + application: Application, +) : AndroidViewModel(application), VideoPlayerViewModel { + + // private + private val mutableUiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) + private var currentContentRatio = 1F + + private var uiVisibilityJob: Job? = null + private var progressUpdaterJob: Job? = null + + //interface + override var callbackListener: VideoPlayerViewModel.Listener? = null + + override var newPlayer: NewPlayer? = null + set(value) { + field = value + installExoPlayer() + } + + override val uiState = mutableUiState.asStateFlow() + + override val player: Player? + get() = newPlayer?.player + + override var minContentRatio: Float = 4F / 3F + set(value) { + if (value <= 0 || maxContentRatio < value) + Log.e( + TAG, + "Ignoring maxContentRatio: It must not be 0 or less and it may not be bigger then mmaxContentRatio. It was Set to: $value" + ) + else { + field = value + mutableUiState.update { it.copy(embeddedUiRatio = getEmbeddedUiRatio()) } + } + } + + + override var maxContentRatio: Float = 16F / 9F + set(value) { + if (value <= 0 || value < minContentRatio) + Log.e( + TAG, + "Ignoring maxContentRatio: It must not be 0 or less and it may not be smaller then minContentRatio. It was Set to: $value" + ) + else { + field = value + mutableUiState.update { it.copy(embeddedUiRatio = getEmbeddedUiRatio()) } + } + } + + override var contentFitMode: ContentScale + get() = mutableUiState.value.contentFitMode + set(value) { + mutableUiState.update { + it.copy(contentFitMode = value) + } + } + + private fun installExoPlayer() { + player?.let { player -> + Log.d(TAG, "Install player: ${player.videoSize.width}") + + player.addListener(object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + Log.d(TAG, "Playing state changed. Is Playing: $isPlaying") + mutableUiState.update { + it.copy(playing = isPlaying) + } + } + + override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) { + super.onVideoSizeChanged(videoSize) + updateContentRatio(VideoSize.fromMedia3VideoSize(videoSize)) + } + + override fun onIsLoadingChanged(isLoading: Boolean) { + super.onIsLoadingChanged(isLoading) + mutableUiState.update { + it.copy(isLoading = isLoading) + } + Log.i( + TAG, + if (isLoading) "Player started loading" else "Player finished loading" + ) + } + }) + } + } + + fun updateContentRatio(videoSize: VideoSize) { + val newRatio = videoSize.getRatio() + val ratio = if (newRatio.isNaN()) currentContentRatio else newRatio + currentContentRatio = ratio + Log.d(TAG, "Update Content ratio: $ratio") + mutableUiState.update { + it.copy( + contentRatio = currentContentRatio, + embeddedUiRatio = getEmbeddedUiRatio() + ) + } + } + + override fun onCleared() { + super.onCleared() + + Log.d(TAG, "viewmodel cleared") + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun initUIState(instanceState: Bundle) { + + val uiState = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) instanceState.getParcelable( + VIDEOPLAYER_UI_STATE, VideoPlayerUIState::class.java + ) + else instanceState.getParcelable(VIDEOPLAYER_UI_STATE) + + uiState?.let { uiState -> + mutableUiState.update { + uiState + } + } + } + + override fun play() { + hideUi() + newPlayer?.play() + } + + override fun pause() { + uiVisibilityJob?.cancel() + newPlayer?.pause() + + } + + override fun prevStream() { + resetHideUiDelayedJob() + Log.e(TAG, "imeplement prev stream") + } + + override fun nextStream() { + resetHideUiDelayedJob() + Log.e(TAG, "implement next stream") + } + + override fun showUi() { + if (mutableUiState.value.fullscreen) + callbackListener?.onUiVissibleToggle(true) + + mutableUiState.update { + it.copy(uiVissible = true) + } + resetHideUiDelayedJob() + resetProgressUpdatePeriodicallyJob() + } + + private fun resetHideUiDelayedJob() { + uiVisibilityJob?.cancel() + uiVisibilityJob = viewModelScope.launch { + delay(4000) + hideUi() + } + } + + private fun resetProgressUpdatePeriodicallyJob() { + progressUpdaterJob?.cancel() + progressUpdaterJob = viewModelScope.launch { + while (true) { + updateProgressOnce() + delay(1000) + } + } + + } + + private fun updateProgressOnce() { + val progress = player?.currentPosition ?: 0 + val duration = player?.duration ?: 1 + val progressPercentage = progress.toFloat() / duration.toFloat() + mutableUiState.update { + it.copy( + seekerPosition = progressPercentage, + durationInMs = duration, + playbackPositionInMs = progress + ) + } + } + + override fun hideUi() { + if (mutableUiState.value.fullscreen) + callbackListener?.onUiVissibleToggle(false) + + progressUpdaterJob?.cancel() + uiVisibilityJob?.cancel() + mutableUiState.update { + it.copy(uiVissible = false) + } + } + + override fun seekPositionChanged(newValue: Float) { + uiVisibilityJob?.cancel() + mutableUiState.update { it.copy(seekerPosition = newValue) } + } + + override fun seekingFinished() { + resetHideUiDelayedJob() + val seekerPosition = mutableUiState.value.seekerPosition + val seekPositionInMs = (player?.duration?.toFloat() ?: 0F) * seekerPosition + player?.seekTo(seekPositionInMs.toLong()) + Log.i(TAG, "Seek to Ms: $seekPositionInMs") + } + + override fun switchToEmbeddedView() { + callbackListener?.onFullscreenToggle(false) + uiVisibilityJob?.cancel() + mutableUiState.update { + it.copy(fullscreen = false, uiVissible = false) + } + } + + override fun switchToFullscreen() { + callbackListener?.onFullscreenToggle(true) + uiVisibilityJob?.cancel() + + mutableUiState.update { + it.copy(fullscreen = true, uiVissible = false) + } + } + + private fun getEmbeddedUiRatio() = + player?.let { player -> + val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio() + return (if (videoRatio.isNaN()) + currentContentRatio + else + videoRatio).coerceIn(minContentRatio, maxContentRatio) + + + } ?: minContentRatio + + + companion object { + val dummy = object : VideoPlayerViewModel { + override var newPlayer: NewPlayer? = null + override val player: Player? = null + override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) + override var minContentRatio = 4F / 3F + override var maxContentRatio = 16F / 9F + override var contentFitMode = ContentScale.FIT_INSIDE + override var callbackListener: VideoPlayerViewModel.Listener? = null + + override fun initUIState(instanceState: Bundle) { + println("dummy impl") + } + + override fun play() { + println("dummy impl") + } + + override fun switchToEmbeddedView() { + println("dummy impl") + } + + override fun switchToFullscreen() { + println("dummy impl") + } + + override fun showUi() { + println("dummy impl") + } + + override fun hideUi() { + println("dummy impl") + } + + override fun seekPositionChanged(newValue: Float) { + println("dummy impl") + } + + override fun seekingFinished() { + println("dummy impl") + } + + override fun pause() { + println("dummy pause") + } + + override fun prevStream() { + println("dummy impl") + } + + override fun nextStream() { + println("dummy impl") + } + } + } +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt index f09e491..6964e4b 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt @@ -59,6 +59,8 @@ fun VideoPlayerControllerUI( uiVissible: Boolean, seekPosition: Float, isLoading: Boolean, + durationInMs: Long, + playbackPositionInMs: Long, play: () -> Unit, pause: () -> Unit, prevStream: () -> Unit, @@ -153,11 +155,13 @@ fun VideoPlayerControllerUI( .defaultMinSize(minHeight = 40.dp) .fillMaxWidth(), isFullscreen = fullscreen, - seekPosition, - switchToFullscreen, - switchToEmbeddedView, - seekPositionChanged, - seekingFinished + durationInMs = durationInMs, + playbackPositionInMs = playbackPositionInMs, + seekPosition = seekPosition, + switchToFullscreen = switchToFullscreen, + switchToEmbeddedView = switchToEmbeddedView, + seekPositionChanged = seekPositionChanged, + seekingFinished = seekingFinished ) } } @@ -197,6 +201,8 @@ fun VideoPlayerControllerUIPreviewEmbedded() { uiVissible = true, seekPosition = 0.3F, isLoading = false, + durationInMs = 9*60*1000, + playbackPositionInMs = 6*60*1000, play = {}, pause = {}, prevStream = {}, @@ -221,6 +227,8 @@ fun VideoPlayerControllerUIPreviewLandscape() { uiVissible = true, seekPosition = 0.3F, isLoading = true, + durationInMs = 9*60*1000, + playbackPositionInMs = 6*60*1000, play = {}, pause = {}, prevStream = {}, @@ -246,6 +254,8 @@ fun VideoPlayerControllerUIPreviewPortrait() { uiVissible = true, seekPosition = 0.3F, isLoading = false, + durationInMs = 9*60*1000, + playbackPositionInMs = 6*60*1000, play = {}, pause = {}, prevStream = {}, 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 03fc780..bde3d66 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 @@ -119,6 +119,8 @@ fun VideoPlayerUI( uiVissible = uiState.uiVissible, seekPosition = uiState.seekerPosition, isLoading = uiState.isLoading, + durationInMs = uiState.durationInMs, + playbackPositionInMs = uiState.playbackPositionInMs, play = viewModel::play, pause = viewModel::pause, prevStream = viewModel::prevStream, diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/theme/Color.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/theme/Color.kt index 86a21cf..fc7b40a 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/theme/Color.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/theme/Color.kt @@ -70,7 +70,7 @@ val video_player_scrim = Color(0xFF000000) @Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") @Composable -fun VideoPlayerControllerUIPreviewEmbeddedColorpreview() { +fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() { VideoPlayerTheme { PreviewBackgroundSurface { VideoPlayerControllerUI(isPlaying = false, @@ -78,6 +78,8 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorpreview() { uiVissible = true, seekPosition = 0.3F, isLoading = false, + durationInMs = 9*60*1000, + playbackPositionInMs = 6*60*1000, play = {}, pause = {}, prevStream = {}, 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 585c1dd..fb598b1 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 @@ -20,6 +20,8 @@ package net.newpipe.newplayer.ui.videoplayer +import android.app.LocaleConfig +import android.icu.text.DecimalFormat import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.material.icons.Icons @@ -30,32 +32,38 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.core.os.ConfigurationCompat import net.newpipe.newplayer.R import net.newpipe.newplayer.ui.seeker.Seeker import net.newpipe.newplayer.ui.theme.VideoPlayerTheme +import java.util.Locale +import kotlin.math.min @Composable fun BottomUI( modifier: Modifier, isFullscreen: Boolean, seekPosition: Float, + durationInMs: Long, + playbackPositionInMs: Long, switchToFullscreen: () -> Unit, switchToEmbeddedView: () -> Unit, seekPositionChanged: (Float) -> Unit, seekingFinished: () -> Unit ) { - Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier ) { - Text("00:06:45") + Text(getTimeStringFromMs(playbackPositionInMs, getLocale() ?: Locale.US)) Seeker( Modifier.weight(1F), value = seekPosition, @@ -65,7 +73,7 @@ fun BottomUI( //Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F)) - Text("00:09:40") + Text(getTimeStringFromMs(durationInMs, getLocale() ?: Locale.US)) IconButton(onClick = if (isFullscreen) switchToEmbeddedView else switchToFullscreen) { Icon( @@ -77,6 +85,42 @@ fun BottomUI( } } +@Composable +@ReadOnlyComposable +fun getLocale(): Locale? { + val configuration = LocalConfiguration.current + return ConfigurationCompat.getLocales(configuration).get(0) +} + + +private const val HOURS_PER_DAY = 24 +private const val MINUTES_PER_HOUR = 60 +private const val SECONDS_PER_MINUTE = 60 +private const val MILLIS_PER_SECOND = 1000 + +private const val MILLIS_PER_DAY = + HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLIS_PER_SECOND +private const val MILLIS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLIS_PER_SECOND +private const val MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND + +private fun getTimeStringFromMs(timeSpanInMs: Long, locale: Locale) : String { + val days = timeSpanInMs / MILLIS_PER_DAY + val millisThisDay = timeSpanInMs - days * MILLIS_PER_DAY + val hours = millisThisDay / MILLIS_PER_HOUR + val millisThisHour = millisThisDay - hours * MILLIS_PER_HOUR + val minutes = millisThisHour / MILLIS_PER_MINUTE + val milliesThisMinute = millisThisHour - minutes * MILLIS_PER_MINUTE + val seconds = milliesThisMinute / MILLIS_PER_SECOND + + + 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) + + return time_string +} + /////////////////////////////////////////////////////////////////// // Preview /////////////////////////////////////////////////////////////////// @@ -90,8 +134,10 @@ fun VideoPlayerControllerBottomUIPreview() { modifier = Modifier, isFullscreen = true, seekPosition = 0.4F, - switchToFullscreen = { }, - switchToEmbeddedView = { }, + durationInMs = 90 * 60 * 1000, + playbackPositionInMs = 3 * 60 * 1000, + switchToFullscreen = { }, + switchToEmbeddedView = { }, seekPositionChanged = {} ) { diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/Menu.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/Menu.kt index 72c2437..1d7cff1 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/Menu.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/Menu.kt @@ -33,6 +33,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -41,12 +42,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import net.newpipe.newplayer.R +import net.newpipe.newplayer.ui.theme.VideoPlayerTheme @Composable fun DropDownMenu() { @@ -123,4 +127,16 @@ fun DropDownMenu() { } } -} \ No newline at end of file + +} +/////////////////////////////////////////////////////////////////// +// Preview +/////////////////////////////////////////////////////////////////// + +@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") +@Composable +fun VideoPlayerControllerDropDownPreview() { + VideoPlayerTheme { + DropDownMenu() + } +}