make duration update work
This commit is contained in:
parent
fdd55bf4a1
commit
a64faae788
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,79 +1,11 @@
|
||||||
/* 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.model
|
package net.newpipe.newplayer.model
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
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 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.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.NewPlayer
|
||||||
import net.newpipe.newplayer.ui.ContentScale
|
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 {
|
interface VideoPlayerViewModel {
|
||||||
var newPlayer: NewPlayer?
|
var newPlayer: NewPlayer?
|
||||||
|
@ -101,300 +33,3 @@ interface VideoPlayerViewModel {
|
||||||
fun onUiVissibleToggle(isVissible: 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,350 @@
|
||||||
|
/* 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.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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,6 +59,8 @@ fun VideoPlayerControllerUI(
|
||||||
uiVissible: Boolean,
|
uiVissible: Boolean,
|
||||||
seekPosition: Float,
|
seekPosition: Float,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
|
durationInMs: Long,
|
||||||
|
playbackPositionInMs: Long,
|
||||||
play: () -> Unit,
|
play: () -> Unit,
|
||||||
pause: () -> Unit,
|
pause: () -> Unit,
|
||||||
prevStream: () -> Unit,
|
prevStream: () -> Unit,
|
||||||
|
@ -153,11 +155,13 @@ fun VideoPlayerControllerUI(
|
||||||
.defaultMinSize(minHeight = 40.dp)
|
.defaultMinSize(minHeight = 40.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
isFullscreen = fullscreen,
|
isFullscreen = fullscreen,
|
||||||
seekPosition,
|
durationInMs = durationInMs,
|
||||||
switchToFullscreen,
|
playbackPositionInMs = playbackPositionInMs,
|
||||||
switchToEmbeddedView,
|
seekPosition = seekPosition,
|
||||||
seekPositionChanged,
|
switchToFullscreen = switchToFullscreen,
|
||||||
seekingFinished
|
switchToEmbeddedView = switchToEmbeddedView,
|
||||||
|
seekPositionChanged = seekPositionChanged,
|
||||||
|
seekingFinished = seekingFinished
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,6 +201,8 @@ fun VideoPlayerControllerUIPreviewEmbedded() {
|
||||||
uiVissible = true,
|
uiVissible = true,
|
||||||
seekPosition = 0.3F,
|
seekPosition = 0.3F,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
durationInMs = 9*60*1000,
|
||||||
|
playbackPositionInMs = 6*60*1000,
|
||||||
play = {},
|
play = {},
|
||||||
pause = {},
|
pause = {},
|
||||||
prevStream = {},
|
prevStream = {},
|
||||||
|
@ -221,6 +227,8 @@ fun VideoPlayerControllerUIPreviewLandscape() {
|
||||||
uiVissible = true,
|
uiVissible = true,
|
||||||
seekPosition = 0.3F,
|
seekPosition = 0.3F,
|
||||||
isLoading = true,
|
isLoading = true,
|
||||||
|
durationInMs = 9*60*1000,
|
||||||
|
playbackPositionInMs = 6*60*1000,
|
||||||
play = {},
|
play = {},
|
||||||
pause = {},
|
pause = {},
|
||||||
prevStream = {},
|
prevStream = {},
|
||||||
|
@ -246,6 +254,8 @@ fun VideoPlayerControllerUIPreviewPortrait() {
|
||||||
uiVissible = true,
|
uiVissible = true,
|
||||||
seekPosition = 0.3F,
|
seekPosition = 0.3F,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
durationInMs = 9*60*1000,
|
||||||
|
playbackPositionInMs = 6*60*1000,
|
||||||
play = {},
|
play = {},
|
||||||
pause = {},
|
pause = {},
|
||||||
prevStream = {},
|
prevStream = {},
|
||||||
|
|
|
@ -119,6 +119,8 @@ fun VideoPlayerUI(
|
||||||
uiVissible = uiState.uiVissible,
|
uiVissible = uiState.uiVissible,
|
||||||
seekPosition = uiState.seekerPosition,
|
seekPosition = uiState.seekerPosition,
|
||||||
isLoading = uiState.isLoading,
|
isLoading = uiState.isLoading,
|
||||||
|
durationInMs = uiState.durationInMs,
|
||||||
|
playbackPositionInMs = uiState.playbackPositionInMs,
|
||||||
play = viewModel::play,
|
play = viewModel::play,
|
||||||
pause = viewModel::pause,
|
pause = viewModel::pause,
|
||||||
prevStream = viewModel::prevStream,
|
prevStream = viewModel::prevStream,
|
||||||
|
|
|
@ -70,7 +70,7 @@ val video_player_scrim = Color(0xFF000000)
|
||||||
|
|
||||||
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerControllerUIPreviewEmbeddedColorpreview() {
|
fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() {
|
||||||
VideoPlayerTheme {
|
VideoPlayerTheme {
|
||||||
PreviewBackgroundSurface {
|
PreviewBackgroundSurface {
|
||||||
VideoPlayerControllerUI(isPlaying = false,
|
VideoPlayerControllerUI(isPlaying = false,
|
||||||
|
@ -78,6 +78,8 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorpreview() {
|
||||||
uiVissible = true,
|
uiVissible = true,
|
||||||
seekPosition = 0.3F,
|
seekPosition = 0.3F,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
durationInMs = 9*60*1000,
|
||||||
|
playbackPositionInMs = 6*60*1000,
|
||||||
play = {},
|
play = {},
|
||||||
pause = {},
|
pause = {},
|
||||||
prevStream = {},
|
prevStream = {},
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
|
|
||||||
package net.newpipe.newplayer.ui.videoplayer
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
@ -30,32 +32,38 @@ import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
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.platform.LocalConfiguration
|
||||||
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 net.newpipe.newplayer.R
|
import net.newpipe.newplayer.R
|
||||||
import net.newpipe.newplayer.ui.seeker.Seeker
|
import net.newpipe.newplayer.ui.seeker.Seeker
|
||||||
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomUI(
|
fun BottomUI(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
isFullscreen: Boolean,
|
isFullscreen: Boolean,
|
||||||
seekPosition: Float,
|
seekPosition: Float,
|
||||||
|
durationInMs: Long,
|
||||||
|
playbackPositionInMs: Long,
|
||||||
switchToFullscreen: () -> Unit,
|
switchToFullscreen: () -> Unit,
|
||||||
switchToEmbeddedView: () -> Unit,
|
switchToEmbeddedView: () -> Unit,
|
||||||
seekPositionChanged: (Float) -> Unit,
|
seekPositionChanged: (Float) -> Unit,
|
||||||
seekingFinished: () -> Unit
|
seekingFinished: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Text("00:06:45")
|
Text(getTimeStringFromMs(playbackPositionInMs, getLocale() ?: Locale.US))
|
||||||
Seeker(
|
Seeker(
|
||||||
Modifier.weight(1F),
|
Modifier.weight(1F),
|
||||||
value = seekPosition,
|
value = seekPosition,
|
||||||
|
@ -65,7 +73,7 @@ fun BottomUI(
|
||||||
|
|
||||||
//Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F))
|
//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) {
|
IconButton(onClick = if (isFullscreen) switchToEmbeddedView else switchToFullscreen) {
|
||||||
Icon(
|
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
|
// Preview
|
||||||
///////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////
|
||||||
|
@ -90,8 +134,10 @@ fun VideoPlayerControllerBottomUIPreview() {
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
isFullscreen = true,
|
isFullscreen = true,
|
||||||
seekPosition = 0.4F,
|
seekPosition = 0.4F,
|
||||||
switchToFullscreen = { },
|
durationInMs = 90 * 60 * 1000,
|
||||||
switchToEmbeddedView = { },
|
playbackPositionInMs = 3 * 60 * 1000,
|
||||||
|
switchToFullscreen = { },
|
||||||
|
switchToEmbeddedView = { },
|
||||||
seekPositionChanged = {}
|
seekPositionChanged = {}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -41,12 +42,15 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.onPlaced
|
import androidx.compose.ui.layout.onPlaced
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.newpipe.newplayer.R
|
import net.newpipe.newplayer.R
|
||||||
|
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DropDownMenu() {
|
fun DropDownMenu() {
|
||||||
|
@ -123,4 +127,16 @@ fun DropDownMenu() {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
// Preview
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
||||||
|
@Composable
|
||||||
|
fun VideoPlayerControllerDropDownPreview() {
|
||||||
|
VideoPlayerTheme {
|
||||||
|
DropDownMenu()
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue