Compare commits

...

10 Commits

25 changed files with 826 additions and 391 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@
.externalNativeBuild
.cxx
local.properties
.kotlin

View File

@ -46,6 +46,7 @@ coil = "2.7.0"
reorderable = "2.4.0-alpha02"
media3Session = "1.4.1"
media3ExoplayerDash = "1.4.1"
adaptiveAndroid = "1.0.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -83,6 +84,7 @@ coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coi
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }
androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3Session" }
androidx-media3-exoplayer-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "media3ExoplayerDash" }
androidx-adaptive-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-android", version.ref = "adaptiveAndroid" }

View File

@ -70,6 +70,7 @@ dependencies {
implementation(libs.reorderable)
implementation(libs.androidx.media3.session)
implementation(libs.androidx.media3.exoplayer.dash)
implementation(libs.androidx.adaptive.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)

View File

@ -4,7 +4,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application>
<application
android:resizeableActivity="true">
<service
android:name=".service.NewPlayerService"
android:foregroundServiceType="mediaPlayback"

View File

@ -38,16 +38,15 @@ interface NewPlayerViewModel {
var contentFitMode: ContentScale
val embeddedPlayerDraggedDownBy: SharedFlow<Float>
val onBackPressed: SharedFlow<Unit>
var deviceInPowerSaveMode: Boolean
fun initUIState(instanceState: Bundle)
fun play()
fun pause()
fun prevStream()
fun nextStream()
fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig)
fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?)
fun onBackPressed()
fun showUi()
fun hideUi()
fun seekPositionChanged(newValue: Float)
fun seekingFinished()
fun embeddedDraggedDown(offset: Float)

View File

@ -18,6 +18,7 @@ open class NewPlayerViewModelDummy : NewPlayerViewModel {
override var contentFitMode = ContentScale.FIT_INSIDE
override val embeddedPlayerDraggedDownBy = MutableSharedFlow<Float>().asSharedFlow()
override val onBackPressed: SharedFlow<Unit> = MutableSharedFlow<Unit>().asSharedFlow()
override var deviceInPowerSaveMode: Boolean = false
override fun initUIState(instanceState: Bundle) {
println("dummy impl")
@ -31,14 +32,6 @@ open class NewPlayerViewModelDummy : NewPlayerViewModel {
println("dummy impl")
}
override fun showUi() {
println("dummy impl")
}
override fun hideUi() {
println("dummy impl")
}
override fun seekPositionChanged(newValue: Float) {
println("dymmy seekPositionChanged: newValue: ${newValue}")
}
@ -115,7 +108,7 @@ open class NewPlayerViewModelDummy : NewPlayerViewModel {
println("dummy impl")
}
override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig) {
override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?) {
println("dummy uiMode change: New UI Mode State: $newUiModeState")
}
}

View File

@ -47,9 +47,11 @@ import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.PlayMode
import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.ui.ContentScale
import java.util.LinkedList
import kotlin.math.abs
val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
@ -70,13 +72,15 @@ class NewPlayerViewModelImpl @Inject constructor(
private var playlistItemToBeMoved: Int? = null
private var playlistItemNewPosition: Int = 0
private var uiVisibilityJob: Job? = null
private var hideUiDelayedJob: Job? = null
private var progressUpdaterJob: Job? = null
private var playlistProgressUpdaterJob: Job? = null
// this is necesary to restore the embedded view UI configuration when returning from fullscreen
private var embeddedUiConfig: EmbeddedUiConfig? = null
private var playbackPositionWhenFastSeekStarted = 0L
private val audioManager =
getSystemService(application.applicationContext, AudioManager::class.java)!!
@ -136,6 +140,15 @@ class NewPlayerViewModelImpl @Inject constructor(
private var mutableOnBackPressed = MutableSharedFlow<Unit>()
override val onBackPressed: SharedFlow<Unit> = mutableOnBackPressed.asSharedFlow()
override var deviceInPowerSaveMode: Boolean = false
get() = field
set(value) {
field = value
if (progressUpdaterJob?.isActive == true) {
startProgressUpdatePeriodicallyJob()
}
}
private fun installNewPlayer() {
newPlayer?.let { newPlayer ->
viewModelScope.launch {
@ -151,9 +164,9 @@ class NewPlayerViewModelImpl @Inject constructor(
it.copy(playing = isPlaying, isLoading = false)
}
if (isPlaying && uiState.value.uiMode.videoControllerUiVisible) {
resetHideUiDelayedJob()
startHideUiDelayedJob()
} else {
uiVisibilityJob?.cancel()
hideUiDelayedJob?.cancel()
}
}
@ -195,12 +208,7 @@ class NewPlayerViewModelImpl @Inject constructor(
val currentMode = mutableUiState.value.uiMode.toPlayMode()
if (currentMode != newMode) {
mutableUiState.update {
it.copy(
uiMode = UIModeState.fromPlayMode(newMode),
embeddedUiConfig = embeddedUiConfig
)
}
changeUiMode(UIModeState.fromPlayMode(newMode), embeddedUiConfig)
}
}
}
@ -299,18 +307,18 @@ class NewPlayerViewModelImpl @Inject constructor(
}
override fun play() {
hideUi()
changeUiMode(uiState.value.uiMode.getUiHiddenState(), null)
newPlayer?.play()
}
override fun pause() {
uiVisibilityJob?.cancel()
hideUiDelayedJob?.cancel()
newPlayer?.pause()
}
override fun prevStream() {
resetHideUiDelayedJob()
startHideUiDelayedJob()
newPlayer?.let { newPlayer ->
if (0 <= newPlayer.currentlyPlayingPlaylistItem - 1) {
newPlayer.currentlyPlayingPlaylistItem -= 1
@ -319,7 +327,7 @@ class NewPlayerViewModelImpl @Inject constructor(
}
override fun nextStream() {
resetHideUiDelayedJob()
startHideUiDelayedJob()
newPlayer?.let { newPlayer ->
if (newPlayer.currentlyPlayingPlaylistItem + 1 <
(newPlayer.exoPlayer.value?.mediaItemCount ?: 0)
@ -329,65 +337,72 @@ class NewPlayerViewModelImpl @Inject constructor(
}
}
override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig) {
if (!uiState.value.uiMode.fullscreen && newUiModeState.fullscreen) {
override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?) {
if (newUiModeState == uiState.value.uiMode) {
return;
}
if (!uiState.value.uiMode.fullscreen && newUiModeState.fullscreen && embeddedUiConfig != null) {
this.embeddedUiConfig = embeddedUiConfig
}
if(!(newUiModeState == UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI ||
newUiModeState == UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI)) {
uiVisibilityJob?.cancel()
if (!(newUiModeState == UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI ||
newUiModeState == UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI)
) {
hideUiDelayedJob?.cancel()
} else {
resetHideUiDelayedJob()
startHideUiDelayedJob()
}
if (newUiModeState.isStreamSelect) {
resetPlaylistProgressUpdaterJob()
}
if (newUiModeState.isChapterSelect) {
resetPlaylistProgressUpdaterJob()
}
if ((uiState.value.uiMode.isStreamSelect || uiState.value.uiMode.isChapterSelect)
&& (!newUiModeState.isStreamSelect && !newUiModeState.isChapterSelect)
) {
startPlaylistProgressUpdaterJob()
} else {
playlistProgressUpdaterJob?.cancel()
}
if (newUiModeState.requiresProgressUpdate) {
startProgressUpdatePeriodicallyJob()
} else {
progressUpdaterJob?.cancel()
}
updateUiMode(newUiModeState)
if (uiState.value.uiMode.fullscreen && !newUiModeState.fullscreen) {
mutableUiState.update {
it.copy(uiMode = newUiModeState, embeddedUiConfig = this.embeddedUiConfig)
}
} else {
mutableUiState.update {
it.copy(uiMode = newUiModeState)
}
}
val newPlayMode = newUiModeState.toPlayMode()
// take the next value from the player because changeUiMode is called when the playBackMode
// of the player changes. If this value was taken from the viewModel instead
// this would lead to an endless loop. of changeMode state calling it self over and over again
// through the callback of the newPlayer?.playBackMode change
val currentPlayMode = newPlayer?.playBackMode?.value ?: PlayMode.IDLE
if (newPlayMode != currentPlayMode) {
newPlayer?.playBackMode?.update {
newPlayMode
}
}
}
override fun showUi() {
mutableUiState.update {
it.copy(uiMode = it.uiMode.getControllerUiVisibleState())
}
resetHideUiDelayedJob()
resetProgressUpdatePeriodicallyJob()
}
private fun resetHideUiDelayedJob() {
var ex:Exception? = null
try {
throw Exception()
} catch(e: Exception) {
ex = e
}
uiVisibilityJob?.cancel()
uiVisibilityJob = viewModelScope.launch {
private fun startHideUiDelayedJob() {
hideUiDelayedJob?.cancel()
hideUiDelayedJob = viewModelScope.launch {
delay(2000)
hideUi()
ex?.printStackTrace()
changeUiMode(uiState.value.uiMode.getUiHiddenState(), null)
}
}
private fun resetProgressUpdatePeriodicallyJob() {
private fun startProgressUpdatePeriodicallyJob() {
progressUpdaterJob?.cancel()
progressUpdaterJob = viewModelScope.launch {
while (true) {
updateProgressOnce()
delay(1000)
delay(if (deviceInPowerSaveMode) 1000 else 1000 / 30/*fps*/)
}
}
}
@ -408,7 +423,7 @@ class NewPlayerViewModelImpl @Inject constructor(
}
}
private fun resetPlaylistProgressUpdaterJob() {
private fun startPlaylistProgressUpdaterJob() {
playlistProgressUpdaterJob?.cancel()
playlistProgressUpdaterJob = viewModelScope.launch {
while (true) {
@ -435,25 +450,24 @@ class NewPlayerViewModelImpl @Inject constructor(
}
}
override fun hideUi() {
progressUpdaterJob?.cancel()
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(uiMode = it.uiMode.getUiHiddenState())
}
}
override fun seekPositionChanged(newValue: Float) {
uiVisibilityJob?.cancel()
mutableUiState.update { it.copy(seekerPosition = newValue) }
}
override fun seekingFinished() {
resetHideUiDelayedJob()
hideUiDelayedJob?.cancel()
progressUpdaterJob?.cancel()
val seekerPosition = mutableUiState.value.seekerPosition
val seekPositionInMs = (newPlayer?.duration?.toFloat() ?: 0F) * seekerPosition
newPlayer?.currentPosition = seekPositionInMs.toLong()
Log.i(TAG, "Seek to Ms: $seekPositionInMs")
mutableUiState.update {
it.copy(
seekerPosition = newValue,
playbackPositionInMs = seekPositionInMs.toLong()
)
}
}
override fun seekingFinished() {
startHideUiDelayedJob()
startProgressUpdatePeriodicallyJob()
}
override fun embeddedDraggedDown(offset: Float) {
@ -461,30 +475,37 @@ class NewPlayerViewModelImpl @Inject constructor(
}
override fun fastSeek(count: Int) {
if(abs(count) == 1) {
playbackPositionWhenFastSeekStarted = newPlayer?.currentPosition ?: 0
}
val fastSeekAmountInS = count * (newPlayer?.fastSeekAmountSec ?: 10)
mutableUiState.update {
it.copy(
fastSeekSeconds = count * (newPlayer?.fastSeekAmountSec ?: 10)
fastSeekSeconds = fastSeekAmountInS
)
}
if (fastSeekAmountInS != 0) {
Log.d(TAG, "fast seeking seeking by $fastSeekAmountInS seconds")
newPlayer?.currentPosition =
playbackPositionWhenFastSeekStarted + (fastSeekAmountInS * 1000)
}
if (mutableUiState.value.uiMode.videoControllerUiVisible) {
resetHideUiDelayedJob()
startHideUiDelayedJob()
}
}
override fun finishFastSeek() {
if (mutableUiState.value.uiMode.videoControllerUiVisible) {
resetHideUiDelayedJob()
startHideUiDelayedJob()
}
val fastSeekAmount = mutableUiState.value.fastSeekSeconds
if (fastSeekAmount != 0) {
Log.d(TAG, "$fastSeekAmount")
newPlayer?.currentPosition = (newPlayer?.currentPosition ?: 0) + (fastSeekAmount * 1000)
mutableUiState.update {
it.copy(fastSeekSeconds = 0)
}
mutableUiState.update {
it.copy(fastSeekSeconds = 0)
}
}
@ -522,7 +543,7 @@ class NewPlayerViewModelImpl @Inject constructor(
override fun onBackPressed() {
val nextMode = uiState.value.uiMode.getNextModeWhenBackPressed()
if (nextMode != null) {
updateUiMode(nextMode)
changeUiMode(nextMode, null)
} else {
safeTryEmit(mutableOnBackPressed, Unit)
}
@ -570,7 +591,7 @@ class NewPlayerViewModelImpl @Inject constructor(
playList = tempList
)
}
resetPlaylistProgressUpdaterJob()
startPlaylistProgressUpdaterJob()
}
override fun onStreamItemDragFinished() {
@ -582,9 +603,9 @@ class NewPlayerViewModelImpl @Inject constructor(
override fun dialogVisible(visible: Boolean) {
if (visible) {
uiVisibilityJob?.cancel()
hideUiDelayedJob?.cancel()
} else {
resetHideUiDelayedJob()
startHideUiDelayedJob()
}
}
@ -592,21 +613,6 @@ class NewPlayerViewModelImpl @Inject constructor(
newPlayer?.removePlaylistItem(uniqueId)
}
private fun updateUiMode(newState: UIModeState) {
val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()
if (newPlayMode != currentPlayMode) {
newPlayer?.playBackMode?.update {
newPlayMode!!
}
} else {
mutableUiState.update {
it.copy(uiMode = newState)
}
}
}
private fun getEmbeddedUiRatio() = newPlayer?.exoPlayer?.value?.let { player ->
val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio()
return (if (videoRatio.isNaN()) currentContentRatio

View File

@ -98,6 +98,24 @@ enum class UIModeState {
else -> false
}
val requiresProgressUpdate: Boolean
get() =
when(this) {
PLACEHOLDER -> false
EMBEDDED_VIDEO -> false
EMBEDDED_VIDEO_CONTROLLER_UI -> true
EMBEDDED_VIDEO_CHAPTER_SELECT -> false
EMBEDDED_VIDEO_STREAM_SELECT -> false
FULLSCREEN_VIDEO -> false
FULLSCREEN_VIDEO_CONTROLLER_UI -> true
FULLSCREEN_VIDEO_CHAPTER_SELECT -> false
FULLSCREEN_VIDEO_STREAM_SELECT -> false
EMBEDDED_AUDIO -> true
FULLSCREEN_AUDIO -> true
AUDIO_CHAPTER_SELECT -> false
AUDIO_STREAM_SELECT -> false
}
// STATE TRANSITIONS
fun getControllerUiVisibleState() =

View File

@ -22,6 +22,7 @@ package net.newpipe.newplayer.ui
import android.app.Activity
import android.content.pm.ActivityInfo
import android.os.Build
import android.util.Log
import android.view.SurfaceView
import androidx.activity.compose.BackHandler
@ -30,6 +31,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.adaptive.currentWindowSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -54,6 +56,7 @@ import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.videoplayer.VideoPlayerUi
import net.newpipe.newplayer.utils.LockScreenOrientation
import net.newpipe.newplayer.utils.getDefaultBrightness
import net.newpipe.newplayer.utils.isInPowerSaveMode
import net.newpipe.newplayer.utils.setScreenBrightness
private const val TAG = "VideoPlayerUI"
@ -112,6 +115,13 @@ fun NewPlayerUI(
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val isInPowerSaveMode = isInPowerSaveMode()
LaunchedEffect(key1 = isInPowerSaveMode) {
viewModel.deviceInPowerSaveMode = isInPowerSaveMode
}
}
if (uiState.uiMode.fitScreenRotation) {
if (uiState.contentRatio < 1) {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
@ -142,10 +152,13 @@ fun NewPlayerUI(
) {
VideoPlayerUi(viewModel = viewModel, uiState = uiState)
} else if (uiState.uiMode == UIModeState.FULLSCREEN_AUDIO ||
uiState.uiMode == UIModeState.EMBEDDED_AUDIO ||
uiState.uiMode == UIModeState.AUDIO_STREAM_SELECT ||
uiState.uiMode == UIModeState.AUDIO_CHAPTER_SELECT
) {
AudioPlayerUI(viewModel = viewModel, uiState = uiState)
val windowSize = currentWindowSize()
AudioPlayerUI(viewModel = viewModel, uiState = uiState,
isLandScape = windowSize.height < windowSize.width)
} else {
LoadingPlaceholder(uiState.embeddedUiRatio)
}

View File

@ -57,19 +57,24 @@ import net.newpipe.newplayer.ui.LoadingPlaceholder
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@androidx.annotation.OptIn(UnstableApi::class)
@OptIn(UnstableApi::class)
@Composable
fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
fun AudioPlaybackController(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState
) {
Row(
modifier = Modifier.background(MaterialTheme.colorScheme.background),
modifier = modifier.background(MaterialTheme.colorScheme.background),
verticalAlignment = Alignment.CenterVertically
) {
//ShuffleModeButton(viewModel = viewModel, uiState = uiState)
Box(modifier = Modifier
.fillMaxWidth()
.aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center
) {
Button(
modifier = Modifier
.fillMaxWidth()
@ -87,10 +92,12 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
}
}
Box(modifier = Modifier
.fillMaxWidth()
.aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center
) {
Button(
modifier = Modifier
.fillMaxWidth()
@ -114,11 +121,14 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
onClick = if (uiState.playing) viewModel::pause else viewModel::play,
shape = CircleShape
) {
if(uiState.isLoading) {
if (uiState.isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(
modifier = Modifier.fillMaxSize().aspectRatio(1F),
color = MaterialTheme.colorScheme.onSurface)
modifier = Modifier
.fillMaxSize()
.aspectRatio(1F),
color = MaterialTheme.colorScheme.onSurface
)
}
} else {
@ -133,10 +143,12 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
}
}
Box(modifier = Modifier
.fillMaxWidth()
.aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center
) {
Button(
modifier = Modifier
.fillMaxWidth()
@ -155,10 +167,12 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
}
}
Box(modifier = Modifier
.fillMaxWidth()
.aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center
) {
Button(
modifier = Modifier
.fillMaxWidth()

View File

@ -0,0 +1,181 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer.ui.audioplayer
import android.app.Activity
import androidx.annotation.OptIn
import androidx.collection.emptyLongSet
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel
import net.newpipe.newplayer.model.NewPlayerViewModelDummy
import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.ui.selection_ui.ITEM_CORNER_SHAPE
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.videoplayer.CONTROLLER_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.videoplayer.PreviewBackgroundSurface
import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.getEmbeddedUiConfig
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
@OptIn(androidx.media3.common.util.UnstableApi::class)
@Composable
fun AudioPlayerEmbeddedUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
val locale = getLocale()!!
val embeddedUIConfig = if (LocalContext.current is Activity)
getEmbeddedUiConfig(activity = LocalContext.current as Activity)
else
EmbeddedUiConfig.DUMMY
Box(modifier = Modifier.wrapContentSize()) {
Thumbnail(
modifier = Modifier.fillMaxWidth(),
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
)
)
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomStart)
) {
Surface(
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.wrapContentSize()
.padding(start = 10.dp, bottom = 14.dp)
) {
Text(
modifier = Modifier.padding(
start = 4.dp,
end = 4.dp,
top = 0.5.dp,
bottom = 0.5.dp
),
text = getTimeStringFromMs(
uiState.playbackPositionInMs,
locale,
leadingZerosForMinutes = false
),
fontSize = 14.sp,
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
)
Surface(
color = CONTROLLER_UI_BACKGROUND_COLOR,
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.wrapContentSize()
.padding(bottom = 14.dp, end = 10.dp)
) {
Text(
modifier = Modifier.padding(
start = 4.dp,
end = 4.dp,
top = 0.5.dp,
bottom = 0.5.dp
),
color = MaterialTheme.colorScheme.onBackground,
text = getTimeStringFromMs(
uiState.durationInMs,
locale,
leadingZerosForMinutes = false
),
fontSize = 14.sp,
)
}
}
LinearProgressIndicator(modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),
progress = {
val duration = if (uiState.durationInMs == 0L) {
0.000000001f
} else {
uiState.durationInMs.toFloat()
}
(uiState.playbackPositionInMs.toFloat() / duration)
})
Surface(
modifier = Modifier
.matchParentSize()
.clickable {
viewModel.changeUiMode(
UIModeState.FULLSCREEN_AUDIO,
embeddedUiConfig = embeddedUIConfig
)
}, color = Color.Transparent
) {
}
}
}
@OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=1080px,dpi=440,orientation=landscape")
@Composable
fun AuidioPlayerEmbeddedPreview() {
VideoPlayerTheme {
PreviewBackgroundSurface {
AudioPlayerEmbeddedUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY,
)
}
}
}

View File

@ -21,27 +21,36 @@
package net.newpipe.newplayer.ui.audioplayer
import android.icu.text.CaseMap.Title
import androidx.annotation.OptIn
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -59,6 +68,8 @@ import net.newpipe.newplayer.ui.selection_ui.StreamSelectUI
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.getInsets
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
private val UI_ENTER_ANIMATION = fadeIn(tween(200))
@ -73,11 +84,12 @@ fun lightAudioControlButtonColorScheme() = ButtonDefaults.buttonColors().copy(
@OptIn(UnstableApi::class)
@Composable
fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState, isLandScape: Boolean) {
val insets = getInsets()
Box(
modifier = Modifier
.fillMaxSize()
.wrapContentSize()
.background(color = MaterialTheme.colorScheme.background)
) {
AnimatedVisibility(
@ -96,6 +108,14 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
StreamSelectUI(viewModel = viewModel, uiState = uiState, shownInAudioPlayer = true)
}
AnimatedVisibility(
visible = uiState.uiMode == UIModeState.EMBEDDED_AUDIO,
enter = UI_ENTER_ANIMATION,
exit = UI_EXIT_ANIMATION
) {
AudioPlayerEmbeddedUI(viewModel = viewModel, uiState = uiState)
}
AnimatedVisibility(
uiState.uiMode == UIModeState.FULLSCREEN_AUDIO,
enter = UI_ENTER_ANIMATION,
@ -107,94 +127,254 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
topBar = {
}) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
Column(
modifier = Modifier
.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
)
Box {
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Thumbnail(
modifier = Modifier.fillMaxWidth(),
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
),
)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
)
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.title.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 6.em
)
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.artist.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 4.em
)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
NewPlayerSeeker(viewModel = viewModel, uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
AudioPlaybackController(viewModel = viewModel, uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
}
AudioBottomUI(viewModel, uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.025f)
)
}
if (isLandScape) {
LandscapeLayout(
viewModel = viewModel,
uiState = uiState,
innerPadding = innerPadding
)
} else {
PortraitLayout(
viewModel = viewModel,
uiState = uiState,
innerPadding = innerPadding
)
}
}
}
}
}
@OptIn(UnstableApi::class)
@Composable
private fun LandscapeLayout(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState,
innerPadding: PaddingValues
) {
Row(modifier = modifier
.fillMaxSize()
.padding(20.dp)) {
Column(
modifier = Modifier
.fillMaxSize()
.weight(1f)
) {
CoverArt(modifier = Modifier
.fillMaxSize()
.weight(0.9f), uiState = uiState)
TitleView(modifier = Modifier
.fillMaxSize()
.weight(0.1f), uiState = uiState)
}
Box(modifier = Modifier.width(20.dp))
Column(
modifier = Modifier
.fillMaxSize()
.weight(1f)
) {
Column(
verticalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.fillMaxSize()
.weight(1f)
) {
ProgressUI(
viewModel = viewModel,
uiState = uiState
)
AudioPlaybackController(
viewModel = viewModel,
uiState = uiState
)
}
AudioBottomUI(viewModel, uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.025f)
)
}
}
}
@OptIn(UnstableApi::class)
@Composable
private fun PortraitLayout(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState,
innerPadding: PaddingValues
) {
Box(
modifier = modifier
.fillMaxSize()
.padding(innerPadding)
) {
Column(
modifier = Modifier
.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
)
CoverArt(uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.3f)
)
TitleView(uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.6f)
)
ProgressUI(viewModel = viewModel, uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
AudioPlaybackController(viewModel = viewModel, uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
}
AudioBottomUI(viewModel, uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.025f)
)
}
}
}
@OptIn(UnstableApi::class)
@Composable
private fun ProgressUI(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState
) {
val locale = getLocale()!!
Column(modifier = modifier) {
NewPlayerSeeker(viewModel = viewModel, uiState = uiState)
Row {
Text(
getTimeStringFromMs(
uiState.playbackPositionInMs,
getLocale() ?: locale
)
)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
)
Text(
getTimeStringFromMs(
uiState.durationInMs,
getLocale() ?: locale
)
)
}
}
}
@OptIn(UnstableApi::class)
@Composable
private fun TitleView(modifier: Modifier = Modifier, uiState: NewPlayerUIState) {
Column(modifier = modifier) {
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.title.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 6.em
)
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.artist.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 4.em
)
}
}
@OptIn(UnstableApi::class)
@Composable
private fun CoverArt(modifier: Modifier = Modifier, uiState: NewPlayerUIState) {
Box {
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Thumbnail(
modifier = Modifier.fillMaxWidth(),
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
),
)
}
}
}
@OptIn(UnstableApi::class)
@Preview(device = "id:pixel_6", showSystemUi = true)
@Composable
fun AudioPlayerUIPreview() {
fun AudioPlayerUIPortraitPreview() {
VideoPlayerTheme {
AudioPlayerUI(viewModel = NewPlayerViewModelDummy(), uiState = NewPlayerUIState.DUMMY)
AudioPlayerUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(uiMode = UIModeState.FULLSCREEN_AUDIO),
isLandScape = false
)
}
}
@OptIn(UnstableApi::class)
@Preview(device = "spec:parent=pixel_6,orientation=landscape", showSystemUi = true)
@Composable
fun AudioPlayerUILandscapePreview() {
VideoPlayerTheme {
AudioPlayerUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(uiMode = UIModeState.FULLSCREEN_AUDIO),
isLandScape = true
)
}
}
@OptIn(UnstableApi::class)
@Preview(device = "spec:parent=pixel_6,orientation=portrait", showSystemUi = true)
@Composable
fun AudioPlayerUIEmbeddedPreview() {
VideoPlayerTheme {
AudioPlayerUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(uiMode = UIModeState.EMBEDDED_AUDIO),
isLandScape = false
)
}
}

View File

@ -56,7 +56,9 @@ val video_player_onErrorContainer = Color(0xFFFFDAD6)
// background color
val video_player_background = Color(0xFF1F1B16)
// Font color on background
val video_player_onBackground = Color(0xFFEAE1D9)
val video_player_surface = Color(0xFF000000)
// The color of the Text and icons
@ -104,13 +106,13 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() {
@OptIn(UnstableApi::class)
@Preview(device = "id:pixel_6")
@Composable
fun AudioPlayerUIPreviewEmbeddedColorPreview() {
fun AudioPlayerUIColorPreview() {
VideoPlayerTheme {
PreviewBackgroundSurface {
AudioPlayerUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(
uiMode = UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI,
uiMode = UIModeState.FULLSCREEN_AUDIO,
playing = true,
seekerPosition = 0.3f,
isLoading = false,
@ -119,6 +121,7 @@ fun AudioPlayerUIPreviewEmbeddedColorPreview() {
bufferedPercentage = 0.4f,
fastSeekSeconds = 10,
),
isLandScape = false
)
}
}

View File

@ -23,7 +23,9 @@ package net.newpipe.newplayer.ui.videoplayer.controller
import android.app.Activity
import androidx.annotation.OptIn
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.FullscreenExit
@ -38,6 +40,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.EmbeddedUiConfig
@ -67,7 +70,13 @@ fun BottomUI(
val locale = getLocale()!!
Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: locale))
NewPlayerSeeker(modifier = Modifier.weight(1F), viewModel = viewModel, uiState = uiState)
NewPlayerSeeker(
modifier = Modifier
.weight(1F)
.padding(start = 4.dp, end = 4.dp),
viewModel = viewModel,
uiState = uiState
)
Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: locale))
@ -112,7 +121,7 @@ fun VideoPlayerControllerBottomUIPreview() {
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(
uiMode = UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI,
seekerPosition = 0.2f,
seekerPosition = 0.0f,
playbackPositionInMs = 3 * 60 * 1000,
bufferedPercentage = 0.4f
),

View File

@ -87,9 +87,9 @@ fun EmbeddedGestureUI(
val defaultOnRegularTap = {
if (uiState.uiMode.videoControllerUiVisible) {
viewModel.hideUi()
viewModel.changeUiMode(uiState.uiMode.getUiHiddenState(), null)
} else {
viewModel.showUi()
viewModel.changeUiMode(uiState.uiMode.getControllerUiVisibleState(), null)
}
}
@ -124,7 +124,7 @@ fun EmbeddedGestureUI(
if (count == 1) {
if (uiState.playing) {
viewModel.pause()
viewModel.showUi()
viewModel.changeUiMode(uiState.uiMode.getControllerUiVisibleState(), null)
} else {
viewModel.play()
}

View File

@ -36,6 +36,8 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -46,6 +48,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel
@ -55,7 +58,7 @@ import net.newpipe.newplayer.utils.getDefaultBrightness
import net.newpipe.newplayer.utils.getEmbeddedUiConfig
private enum class IndicatorMode {
NONE, VOLUME_INDICATOR_VISSIBLE, BRIGHTNESS_INDICATOR_VISSIBLE
NONE, VOLUME_INDICATOR_VISIBLE, BRIGHTNESS_INDICATOR_VISIBLE
}
@OptIn(UnstableApi::class)
@ -65,7 +68,7 @@ fun FullscreenGestureUI(
) {
var heightPx by remember {
mutableStateOf(0f)
mutableFloatStateOf(0f)
}
var indicatorMode by remember {
@ -74,9 +77,9 @@ fun FullscreenGestureUI(
val defaultOnRegularTap = {
if (uiState.uiMode.videoControllerUiVisible) {
viewModel.hideUi()
viewModel.changeUiMode(uiState.uiMode.getUiHiddenState(), null)
} else {
viewModel.showUi()
viewModel.changeUiMode(uiState.uiMode.getControllerUiVisibleState(), null)
}
}
@ -100,8 +103,8 @@ fun FullscreenGestureUI(
indicatorMode = IndicatorMode.NONE
},
onMovement = { change ->
if (indicatorMode == IndicatorMode.NONE || indicatorMode == IndicatorMode.BRIGHTNESS_INDICATOR_VISSIBLE) {
indicatorMode = IndicatorMode.BRIGHTNESS_INDICATOR_VISSIBLE
if (indicatorMode == IndicatorMode.NONE || indicatorMode == IndicatorMode.BRIGHTNESS_INDICATOR_VISIBLE) {
indicatorMode = IndicatorMode.BRIGHTNESS_INDICATOR_VISIBLE
if (heightPx != 0f) {
viewModel.brightnessChange(-change.y / heightPx, defaultBrightness)
@ -131,7 +134,7 @@ fun FullscreenGestureUI(
if(count == 1) {
if(uiState.playing) {
viewModel.pause()
viewModel.showUi()
viewModel.changeUiMode(uiState.uiMode.getControllerUiVisibleState(), null)
} else {
viewModel.play()
}
@ -145,8 +148,8 @@ fun FullscreenGestureUI(
indicatorMode = IndicatorMode.NONE
},
onMovement = { change ->
if (indicatorMode == IndicatorMode.NONE || indicatorMode == IndicatorMode.VOLUME_INDICATOR_VISSIBLE) {
indicatorMode = IndicatorMode.VOLUME_INDICATOR_VISSIBLE
if (indicatorMode == IndicatorMode.NONE || indicatorMode == IndicatorMode.VOLUME_INDICATOR_VISIBLE) {
indicatorMode = IndicatorMode.VOLUME_INDICATOR_VISIBLE
if (heightPx != 0f) {
viewModel.volumeChange(-change.y / heightPx)
}
@ -166,14 +169,14 @@ fun FullscreenGestureUI(
IndicatorAnimation(
modifier = Modifier.align(Alignment.Center),
visible = indicatorMode == IndicatorMode.VOLUME_INDICATOR_VISSIBLE,
visible = indicatorMode == IndicatorMode.VOLUME_INDICATOR_VISIBLE,
) {
VolumeCircle(volumeFraction = uiState.soundVolume)
}
IndicatorAnimation(
modifier = Modifier.align(Alignment.Center),
visible = indicatorMode == IndicatorMode.BRIGHTNESS_INDICATOR_VISSIBLE,
visible = indicatorMode == IndicatorMode.BRIGHTNESS_INDICATOR_VISIBLE,
) {
VolumeCircle(
volumeFraction = uiState.brightness ?: defaultBrightness,
@ -243,15 +246,15 @@ fun FullscreenGestureUIPreview() {
fun FullscreenGestureUIPreviewInteractive() {
var seekSeconds by remember {
mutableStateOf(0)
mutableIntStateOf(0)
}
var brightnessValue by remember {
mutableStateOf(0f)
mutableFloatStateOf(0f)
}
var soundVolume by remember {
mutableStateOf(0f)
mutableFloatStateOf(0f)
}
var uiVisible by remember {
@ -264,12 +267,12 @@ fun FullscreenGestureUIPreviewInteractive() {
modifier = Modifier,
@OptIn(UnstableApi::class)
object : NewPlayerViewModelDummy() {
override fun hideUi() {
uiVisible = false
}
override fun showUi() {
uiVisible = true
override fun changeUiMode(
newUiModeState: UIModeState,
embeddedUiConfig: EmbeddedUiConfig?
) {
super.changeUiMode(newUiModeState, embeddedUiConfig)
uiVisible = newUiModeState.videoControllerUiVisible
}
override fun fastSeek(steps: Int) {
@ -299,7 +302,7 @@ fun FullscreenGestureUIPreviewInteractive() {
}
AnimatedVisibility(uiVisible) {
Text("UI is Vissible")
Text("UI is Visible")
}
}
}

View File

@ -1,3 +1,24 @@
/* 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.utils
data class VideoSize(

View File

@ -24,25 +24,23 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.graphics.drawable.shapes.Shape
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.view.WindowManager
import androidx.annotation.OptIn
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.waterfall
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
@ -184,6 +182,13 @@ fun Thumbnail(
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@Composable
fun isInPowerSaveMode() =
(LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager)
.isPowerSaveMode
@OptIn(UnstableApi::class)
fun getPlaylistDurationInMS(playlist: List<MediaItem>) : Long {
var duration = 0L

View File

@ -26,6 +26,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import net.newpipe.newplayer.ActivityBrainSlug
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.PlayMode
@ -53,42 +54,54 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.start6502StreamButton.setOnClickListener {
binding.buttons.start6502StreamButton.setOnClickListener {
newPlayer.playWhenReady = true
newPlayer.playStream("6502", PlayMode.EMBEDDED_VIDEO)
}
binding.startImuStreamButton.setOnClickListener {
binding.buttons.startImuStreamButton.setOnClickListener {
newPlayer.playWhenReady = true
newPlayer.playStream("imu", PlayMode.EMBEDDED_VIDEO)
}
binding.startPortraitStreamButton.setOnClickListener {
binding.buttons.startPortraitStreamButton.setOnClickListener {
newPlayer.playWhenReady = true
newPlayer.playStream("portrait", PlayMode.EMBEDDED_VIDEO)
}
binding.startYtTestVideoButton.setOnClickListener {
binding.buttons.startYtTestVideoButton.setOnClickListener {
newPlayer.playWhenReady = true
newPlayer.playStream("yt_test", PlayMode.EMBEDDED_VIDEO)
}
binding.add6502StreamButton.setOnClickListener {
binding.buttons.add6502StreamButton.setOnClickListener {
newPlayer.addToPlaylist("6502")
}
binding.addImuStreamButton.setOnClickListener {
binding.buttons.addImuStreamButton.setOnClickListener {
newPlayer.addToPlaylist("imu")
}
binding.addPortraitStreamButton.setOnClickListener {
binding.buttons.addPortraitStreamButton.setOnClickListener {
newPlayer.addToPlaylist("portrait")
}
binding.addYtTestVideoButton.setOnClickListener {
binding.buttons.addYtTestVideoButton.setOnClickListener {
newPlayer.addToPlaylist("yt_test")
}
binding.buttons.listenModeButton.setOnClickListener {
newPlayer.playBackMode.update {
PlayMode.FULLSCREEN_AUDIO
}
}
binding.buttons.pipModeButton.setOnClickListener {
newPlayer.playBackMode.update {
PlayMode.PIP
}
}
newPlayerViewModel.newPlayer = newPlayer
newPlayerViewModel.contentFitMode = ContentScale.FIT_INSIDE

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="#e8eaed" android:pathData="M360,840L200,840q-33,0 -56.5,-23.5T120,760v-280q0,-75 28.5,-140.5t77,-114q48.5,-48.5 114,-77T480,120q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,480v280q0,33 -23.5,56.5T760,840L600,840v-320h160v-40q0,-117 -81.5,-198.5T480,200q-117,0 -198.5,81.5T200,480v40h160v320ZM280,600h-80v160h80v-160ZM680,600v160h80v-160h-80ZM280,600h-80,80ZM680,600h80,-80Z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="#e8eaed" android:pathData="M160,800q-33,0 -56.5,-23.5T80,720v-480q0,-33 23.5,-56.5T160,160h640q33,0 56.5,23.5T880,240v480q0,33 -23.5,56.5T800,800L160,800ZM160,720h640v-480L160,240v480ZM160,720v-480,480ZM440,520h320v-240L440,280v240ZM520,440v-80h160v80L520,440Z"/>
</vector>

View File

@ -73,73 +73,11 @@
app:layout_constraintStart_toEndOf="@id/embedded_player_layout"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/buttons"
layout="@layout/buttons" />
<Button
android:id="@+id/start_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start 6502 Stream" />
<Button
android:id="@+id/start_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start IMU Stream" />
<Button
android:id="@+id/start_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Portrait Stream" />
<Button
android:id="@+id/start_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Yt Test Video" />
<Button
android:id="@+id/add_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append 6502 Stream" />
<Button
android:id="@+id/add_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append IMU Stream" />
<Button
android:id="@+id/add_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append Portrait Stream" />
<Button
android:id="@+id/add_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Add Yt Test Video" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -40,14 +40,13 @@
<LinearLayout
android:id="@+id/embedded_player_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/buttons_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_weight="1">
app:layout_constraintVertical_bias="0.0">
<net.newpipe.newplayer.VideoPlayerView
android:id="@+id/embedded_player"
@ -72,73 +71,9 @@
app:layout_constraintTop_toBottomOf="@id/embedded_player_layout"
app:layout_constraintVertical_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/start_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start 6502 Stream" />
<Button
android:id="@+id/start_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start IMU Stream" />
<Button
android:id="@+id/start_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Portrait Stream" />
<Button
android:id="@+id/start_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Yt Test Video" />
<Button
android:id="@+id/add_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append 6502 Stream" />
<Button
android:id="@+id/add_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append IMU Stream" />
<Button
android:id="@+id/add_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append Portrait Stream" />
<Button
android:id="@+id/add_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Add Yt Test Video" />
</LinearLayout>
<include
android:id="@+id/buttons"
layout="@layout/buttons"/>
</ScrollView>

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="@+id/listen_mode_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/headphones"
android:contentDescription="Headphone mode" />
<ImageButton
android:id="@+id/pip_mode_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/pip"
android:contentDescription="Picture in Picture mode" />
</LinearLayout>
<Button
android:id="@+id/start_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start 6502 Stream" />
<Button
android:id="@+id/start_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start IMU Stream" />
<Button
android:id="@+id/start_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Portrait Stream" />
<Button
android:id="@+id/start_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Yt Test Video" />
<Button
android:id="@+id/add_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append 6502 Stream" />
<Button
android:id="@+id/add_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append IMU Stream" />
<Button
android:id="@+id/add_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append Portrait Stream" />
<Button
android:id="@+id/add_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Add Yt Test Video" />
</LinearLayout>

View File

@ -33,7 +33,6 @@
<item>1200000</item>
<item>1800000</item>
<item>2400000</item>
<item>3600000</item>
</integer-array>
<integer name="ccc_6502_length">3116</integer>