Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.theme.VideoPlayerDarkTheme

@Composable

Expand All @@ -64,7 +64,7 @@ internal fun LoadingPlaceholder(aspectRatio: Float = 3F / 1F) {
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
@Composable
private fun VideoPlayerLoaidingPlaceholderPreview() {
VideoPlayerTheme {
VideoPlayerDarkTheme {
LoadingPlaceholder()
}
}
208 changes: 115 additions & 93 deletions new-player/src/main/java/net/newpipe/newplayer/ui/NewPlayerUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import android.os.Build
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.annotation.OptIn
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.adaptive.currentWindowSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand All @@ -35,6 +38,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.compose.material3.Typography


import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.data.NewPlayerException
Expand All @@ -43,15 +48,16 @@ import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel
import net.newpipe.newplayer.uiModel.NewPlayerViewModel
import net.newpipe.newplayer.uiModel.NewPlayerViewModelDummy
import net.newpipe.newplayer.ui.audioplayer.AudioPlayerUI
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.theme.VideoPlayerDarkTheme
import net.newpipe.newplayer.ui.videoplayer.VideoPlayerUi
import net.newpipe.newplayer.ui.common.LockScreenOrientation
import net.newpipe.newplayer.ui.common.activity
import net.newpipe.newplayer.ui.common.findActivity
import net.newpipe.newplayer.ui.common.getDefaultBrightness
import net.newpipe.newplayer.ui.common.isInPowerSaveMode
import net.newpipe.newplayer.ui.common.setScreenBrightness
import net.newpipe.newplayer.ui.common.window
import net.newpipe.newplayer.ui.theme.NewPlayerTypography
import net.newpipe.newplayer.ui.theme.NewPlayerDarkColorScheme
import net.newpipe.newplayer.ui.theme.NewPlayerLightColorScheme

private const val TAG = "VideoPlayerUI"

Expand All @@ -65,122 +71,138 @@ private const val TAG = "VideoPlayerUI"
* or views are hidden and only NewPlayerUI is visible. You can read more about this in
* the [NewPlayerViewModel], since the [viewModel] is responsible to tell your UI how to behave
* in such cases.
*
* @param viewModel the NewPlayerViewModel that should control this NewPlayerUi.
* @param lightColorScheme is the color scheme used by non video ui related composables, when the system is in light mode.
* @param darkColorScheme is the color scheme used by the video ui related composables, and the rest of the ui if the system is in dark mode.
* @param the typography used by NewPlayer
*/
@OptIn(UnstableApi::class)
@Composable
fun NewPlayerUI(
viewModel: NewPlayerViewModel?,
lightColorScheme: ColorScheme = NewPlayerLightColorScheme,
darkColorScheme: ColorScheme = NewPlayerDarkColorScheme,
typography: Typography = NewPlayerTypography
) {
if (viewModel !is InternalNewPlayerViewModel?) {
throw NewPlayerException(
"The view model given to NewPlayerUI must be of type InternalNewPlayerViewModel. "
+ "This can not be implemented externally, so do not extend NewPlayerViewModel"
)
}

if (viewModel == null) {
LoadingPlaceholder()
} else if (viewModel.newPlayer == null) {
LoadingPlaceholder(viewModel.uiState.collectAsState().value.embeddedUiRatio)
} else {
val uiState by viewModel.uiState.collectAsState()
MaterialTheme(colorScheme = darkColorScheme, typography = typography) {
if (viewModel !is InternalNewPlayerViewModel?) {
throw NewPlayerException(
"The view model given to NewPlayerUI must be of type InternalNewPlayerViewModel. "
+ "This can not be implemented externally, so do not extend NewPlayerViewModel"
)
}

// find out whether application is light or dark mode from LocalContext.current
val view = LocalView.current
val activity = activity()
val window = activity.window
if (viewModel == null) {
LoadingPlaceholder()
} else if (viewModel.newPlayer == null) {
LoadingPlaceholder(viewModel.uiState.collectAsState().value.embeddedUiRatio)
} else {
val uiState by viewModel.uiState.collectAsState()

// Setup fullscreen
// find out whether application is light or dark mode from LocalContext.current
val view = LocalView.current
val activity = activity()
val window = activity.window

// Setup fullscreen

LaunchedEffect(uiState.uiMode.fullscreen) {
if (uiState.uiMode.fullscreen) {
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
false
} else {
uiState.embeddedUiConfig?.let {
val isInDarkTheme = isSystemInDarkTheme()
LaunchedEffect(uiState.uiMode.forceSysUiDarkThemeMode, key2 = isInDarkTheme) {
if (uiState.uiMode.forceSysUiDarkThemeMode) {
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
it.systemBarInLightMode
false
} else {
uiState.embeddedUiConfig?.let {
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = isInDarkTheme
}
}
}
}

if (uiState.uiMode.fullscreen) {
BackHandler {
viewModel.onBackPressed()
if (uiState.uiMode.fullscreen) {
BackHandler {
viewModel.onBackPressed()
}
}
}

// setup immersive mode
LaunchedEffect(
key1 = uiState.uiMode.systemInsetsVisible,
) {
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT

if (uiState.uiMode.systemInsetsVisible) {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
} else {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
// setup immersive mode
LaunchedEffect(
key1 = uiState.uiMode.systemInsetsVisible,
) {
val windowInsetsController =
WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT

if (uiState.uiMode.systemInsetsVisible) {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
} else {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
}
}
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val isInPowerSaveMode = isInPowerSaveMode()
LaunchedEffect(key1 = isInPowerSaveMode) {
viewModel.deviceInPowerSaveMode = isInPowerSaveMode
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)
if (uiState.uiMode.fitScreenRotation) {
if (uiState.contentRatio < 1) {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
} else {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
}
} else {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
}
} else {
uiState.embeddedUiConfig?.let {
LockScreenOrientation(orientation = it.screenOrientation)
uiState.embeddedUiConfig?.let {
LockScreenOrientation(orientation = it.screenOrientation)
}
}
}

val defaultBrightness = activity.getDefaultBrightness()
LaunchedEffect(key1 = uiState.brightness) {
Log.d(TAG, "New Brightness: ${uiState.brightness}")

setScreenBrightness(
if (uiState.brightness < 0) defaultBrightness else uiState.brightness, activity
)
}
val defaultBrightness = activity.getDefaultBrightness()
LaunchedEffect(key1 = uiState.brightness) {
Log.d(TAG, "New Brightness: ${uiState.brightness}")

when (uiState.uiMode) {

UIModeState.FULLSCREEN_VIDEO,
UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI,
UIModeState.FULLSCREEN_VIDEO_CHAPTER_SELECT,
UIModeState.FULLSCREEN_VIDEO_STREAM_SELECT,
UIModeState.EMBEDDED_VIDEO,
UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI,
UIModeState.EMBEDDED_VIDEO_STREAM_SELECT,
UIModeState.EMBEDDED_VIDEO_CHAPTER_SELECT,
UIModeState.PIP -> {
VideoPlayerUi(viewModel = viewModel, uiState = uiState)
}

UIModeState.FULLSCREEN_AUDIO,
UIModeState.EMBEDDED_AUDIO,
UIModeState.AUDIO_STREAM_SELECT,
UIModeState.AUDIO_CHAPTER_SELECT -> {
val windowSize = currentWindowSize()
AudioPlayerUI(
viewModel = viewModel, uiState = uiState,
isLandScape = windowSize.height < windowSize.width
setScreenBrightness(
if (uiState.brightness < 0) defaultBrightness else uiState.brightness, activity
)
}

else -> {
LoadingPlaceholder(uiState.embeddedUiRatio)
when (uiState.uiMode) {

UIModeState.FULLSCREEN_VIDEO,
UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI,
UIModeState.FULLSCREEN_VIDEO_CHAPTER_SELECT,
UIModeState.FULLSCREEN_VIDEO_STREAM_SELECT,
UIModeState.EMBEDDED_VIDEO,
UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI,
UIModeState.EMBEDDED_VIDEO_STREAM_SELECT,
UIModeState.EMBEDDED_VIDEO_CHAPTER_SELECT,
UIModeState.PIP -> {
VideoPlayerUi(viewModel = viewModel, uiState = uiState)
}

UIModeState.FULLSCREEN_AUDIO,
UIModeState.EMBEDDED_AUDIO,
UIModeState.AUDIO_STREAM_SELECT,
UIModeState.AUDIO_CHAPTER_SELECT -> {
val windowSize = currentWindowSize()
MaterialTheme(
colorScheme =
if (isInDarkTheme) darkColorScheme else lightColorScheme,
typography = typography
) {
AudioPlayerUI(
viewModel = viewModel, uiState = uiState,
isLandScape = windowSize.height < windowSize.width
)
}
}

else -> {
LoadingPlaceholder(uiState.embeddedUiRatio)
}
}
}
}
Expand All @@ -191,7 +213,7 @@ fun NewPlayerUI(
@Preview(device = "spec:width=1080px,height=700px,dpi=440,orientation=landscape")
@Composable
private fun PlayerUIPreviewEmbedded() {
VideoPlayerTheme {
VideoPlayerDarkTheme {
NewPlayerUI(viewModel = NewPlayerViewModelDummy())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ import dagger.hilt.android.AndroidEntryPoint
import net.newpipe.newplayer.R
import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel
import net.newpipe.newplayer.uiModel.NewPlayerViewModel
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.theme.VideoPlayerDarkTheme
import net.newpipe.newplayer.data.NewPlayerException
import net.newpipe.newplayer.ui.theme.VideoPlayerLightTheme


/**
Expand All @@ -49,7 +50,7 @@ class NewPlayerView : FrameLayout {
applyViewModel()
}

private val composeView:ComposeView
private val composeView: ComposeView

@JvmOverloads
constructor(
Expand All @@ -67,9 +68,7 @@ class NewPlayerView : FrameLayout {
composeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
VideoPlayerTheme {
NewPlayerUI(viewModel = viewModel as InternalNewPlayerViewModel?)
}
NewPlayerUI(viewModel = viewModel as InternalNewPlayerViewModel?)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import net.newpipe.newplayer.R
import net.newpipe.newplayer.uiModel.NewPlayerUIState
import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel
import net.newpipe.newplayer.uiModel.NewPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.theme.VideoPlayerDarkTheme

@androidx.annotation.OptIn(UnstableApi::class)
@Composable
Expand Down Expand Up @@ -200,7 +200,7 @@ internal fun AudioPlaybackControllerUI(
@Preview(device = "id:pixel_6")
@Composable
private fun AudioPlayerControllerPreview() {
VideoPlayerTheme {
VideoPlayerDarkTheme {
AudioPlaybackControllerUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(playList = emptyList(), isLoading = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import net.newpipe.newplayer.uiModel.NewPlayerUIState
import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel
import net.newpipe.newplayer.uiModel.NewPlayerViewModelDummy
import net.newpipe.newplayer.uiModel.UIModeState
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.theme.VideoPlayerDarkTheme
import net.newpipe.newplayer.ui.videoplayer.CONTROLLER_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.videoplayer.PreviewBackgroundSurface
import net.newpipe.newplayer.ui.common.Thumbnail
Expand All @@ -59,7 +59,7 @@ import net.newpipe.newplayer.ui.common.getTimeStringFromMs

/** @hide */
internal fun AudioPlayerEmbeddedUI(viewModel: InternalNewPlayerViewModel, uiState: NewPlayerUIState) {
val locale = getLocale()!!
val locale = getLocale()

val embeddedUIConfig = getEmbeddedUiConfig()

Expand Down Expand Up @@ -119,7 +119,7 @@ internal fun AudioPlayerEmbeddedUI(viewModel: InternalNewPlayerViewModel, uiStat
top = 0.5.dp,
bottom = 0.5.dp
),
color = MaterialTheme.colorScheme.onBackground,
color = MaterialTheme.colorScheme.onPrimary,
text = getTimeStringFromMs(
uiState.durationInMs,
locale,
Expand Down Expand Up @@ -162,7 +162,7 @@ internal fun AudioPlayerEmbeddedUI(viewModel: InternalNewPlayerViewModel, uiStat
@Preview(device = "spec:width=1080px,height=1080px,dpi=440,orientation=landscape")
@Composable
private fun AudioPlayerEmbeddedPreview() {
VideoPlayerTheme {
VideoPlayerDarkTheme {
PreviewBackgroundSurface {
AudioPlayerEmbeddedUI(
viewModel = NewPlayerViewModelDummy(),
Expand Down
Loading
Loading