From 8f78d72a133fca444e6c543246dda7ccff3b7ec1 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 26 Aug 2024 16:49:03 +0200 Subject: [PATCH] make activity brainslug also access uiState This operation introduces a glitch since the composable and the views are updated simultaniously. However this leads to a situation where the embedded view thinkgs its fullscreen and thus renders alike. Due to this reason the embbedded view breafly jumps up. --- .../newpipe/newplayer/ActivityBrainSlug.kt | 9 ++- .../net/newpipe/newplayer/VideoPlayerView.kt | 2 +- .../newplayer/model/EmbeddedUiConfig.kt | 34 +++++++++ .../newpipe/newplayer/model/UIModeState.kt | 20 ++++++ .../newplayer/model/VideoPlayerUIState.kt | 8 +-- .../newplayer/model/VideoPlayerViewModel.kt | 2 +- .../model/VideoPlayerViewModelImpl.kt | 27 +++++-- .../model/ViewoPlayerViewModelDummy.kt | 7 +- .../ui/VideoPlayerLoadingPlaceholder.kt | 15 ++-- .../net/newpipe/newplayer/ui/VideoPlayerUI.kt | 71 ++++++++++++++----- .../java/net/newpipe/newplayer/utils/utils.kt | 10 +-- 11 files changed, 160 insertions(+), 45 deletions(-) create mode 100644 new-player/src/main/java/net/newpipe/newplayer/model/EmbeddedUiConfig.kt diff --git a/new-player/src/main/java/net/newpipe/newplayer/ActivityBrainSlug.kt b/new-player/src/main/java/net/newpipe/newplayer/ActivityBrainSlug.kt index 0a80831..45327a8 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ActivityBrainSlug.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ActivityBrainSlug.kt @@ -26,10 +26,11 @@ import androidx.core.view.WindowInsetsCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import net.newpipe.newplayer.model.VideoPlayerViewModel +private const val TAG = "ActivityBrainSlug" + class ActivityBrainSlug(val viewModel: VideoPlayerViewModel) { val brainSlugScope = CoroutineScope(Dispatchers.Main + Job()) @@ -78,10 +79,16 @@ class ActivityBrainSlug(val viewModel: VideoPlayerViewModel) { removeSystemInsets() viewsToHideOnFullscreen.forEach { it.visibility = View.GONE } fullscreenPlayerView?.visibility = View.VISIBLE + embeddedPlayerView?.visibility = View.GONE + fullscreenPlayerView?.viewModel = viewModel + embeddedPlayerView?.viewModel = null } else { addSystemInsets() viewsToHideOnFullscreen.forEach { it.visibility = View.VISIBLE } fullscreenPlayerView?.visibility = View.GONE + embeddedPlayerView?.visibility = View.VISIBLE + fullscreenPlayerView?.viewModel = null + embeddedPlayerView?.viewModel = viewModel } } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/VideoPlayerView.kt b/new-player/src/main/java/net/newpipe/newplayer/VideoPlayerView.kt index df5e22b..4773fb3 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/VideoPlayerView.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/VideoPlayerView.kt @@ -49,7 +49,7 @@ class VideoPlayerView : FrameLayout { defStyleAttr: Int = 0 ) : super(context, attrs, defStyleAttr) { val view = LayoutInflater.from(context).inflate(R.layout.video_player_view, this) - composeView = view.findViewById(R.id.video_player_compose_view) + composeView = view.findViewById(R.id.video_player_compose_view) applyViewModel() } diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/EmbeddedUiConfig.kt b/new-player/src/main/java/net/newpipe/newplayer/model/EmbeddedUiConfig.kt new file mode 100644 index 0000000..af24d6d --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/model/EmbeddedUiConfig.kt @@ -0,0 +1,34 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + +package net.newpipe.newplayer.model + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * Restores the embedded mode UI config when returning from fullscreen + */ +@Parcelize +data class EmbeddedUiConfig( + val systemBarInLightMode: Boolean, + val brightness: Float, + val screenOrientation: Int +) : Parcelable \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/UIModeState.kt b/new-player/src/main/java/net/newpipe/newplayer/model/UIModeState.kt index bbbfe37..38f08ac 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/UIModeState.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/UIModeState.kt @@ -1,3 +1,23 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + package net.newpipe.newplayer.model import net.newpipe.newplayer.PlayMode diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt index 3b4b108..160806d 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt @@ -38,9 +38,8 @@ data class VideoPlayerUIState( val playbackPositionInMs: Long, val fastSeekSeconds: Int, val soundVolume: Float, - - // when null use system value - val brightness: Float? + val brightness: Float?, // when null use system value + val embeddedUiConfig: EmbeddedUiConfig? ) : Parcelable { companion object { val DEFAULT = VideoPlayerUIState( @@ -59,7 +58,8 @@ data class VideoPlayerUIState( playbackPositionInMs = 0, fastSeekSeconds = 0, soundVolume = 0f, - brightness = null + brightness = null, + embeddedUiConfig = null ) } } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt index 0240a43..a90f0cf 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.StateFlow import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.ui.ContentScale - interface VideoPlayerViewModel { var newPlayer: NewPlayer? val internalPlayer: Player? @@ -53,4 +52,5 @@ interface VideoPlayerViewModel { fun finishFastSeek() fun brightnessChange(changeRate: Float, systemBrightness: Float) fun volumeChange(changeRate: Float) + fun onReportEmbeddedConfig(embeddedUiConfig: EmbeddedUiConfig?) } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt index b57738a..4534dfd 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt @@ -26,19 +26,16 @@ import android.os.Build import android.os.Bundle import android.util.Log import androidx.annotation.RequiresApi -import androidx.collection.mutableFloatFloatMapOf import androidx.core.content.ContextCompat.getSystemService import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.Player import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import javax.inject.Inject import kotlinx.coroutines.flow.asStateFlow @@ -52,6 +49,7 @@ val VIDEOPLAYER_UI_STATE = "video_player_ui_state" private const val TAG = "VideoPlayerViewModel" + @HiltViewModel class VideoPlayerViewModelImpl @Inject constructor( private val savedStateHandle: SavedStateHandle, @@ -121,7 +119,7 @@ class VideoPlayerViewModelImpl @Inject constructor( } } - var mutableEmbeddedPlayerDraggedDownBy = MutableSharedFlow () + var mutableEmbeddedPlayerDraggedDownBy = MutableSharedFlow() override val embeddedPlayerDraggedDownBy = mutableEmbeddedPlayerDraggedDownBy.asSharedFlow() private fun installNewPlayer() { @@ -194,7 +192,7 @@ class VideoPlayerViewModelImpl @Inject constructor( ) else instanceState.getParcelable(VIDEOPLAYER_UI_STATE) - if(recoveredUiState != null) { + if (recoveredUiState != null) { mutableUiState.update { recoveredUiState } @@ -317,6 +315,7 @@ class VideoPlayerViewModelImpl @Inject constructor( } override fun brightnessChange(changeRate: Float, systemBrightness: Float) { + if (mutableUiState.value.uiMode.fullscreen) { val currentBrightness = mutableUiState.value.brightness ?: if (systemBrightness < 0f) 0.5f else systemBrightness @@ -347,6 +346,21 @@ class VideoPlayerViewModelImpl @Inject constructor( } } + override fun onReportEmbeddedConfig(embeddedUiConfig: EmbeddedUiConfig?) { + if (embeddedUiConfig == null) { + mutableUiState.update { + it.copy(embeddedUiConfig = null) + } + } else { + if (uiState.value.embeddedUiConfig == null) { + println("gurken: ${embeddedUiConfig}") + mutableUiState.update { + it.copy(embeddedUiConfig = embeddedUiConfig) + } + } + } + } + override fun switchToEmbeddedView() { uiVisibilityJob?.cancel() finishFastSeek() @@ -356,13 +370,14 @@ class VideoPlayerViewModelImpl @Inject constructor( override fun switchToFullscreen() { uiVisibilityJob?.cancel() finishFastSeek() + updateUiMode(UIModeState.FULLSCREEN_VIDEO) } private fun updateUiMode(newState: UIModeState) { val newPlayMode = newState.toPlayMode() val currentPlayMode = mutableUiState.value.uiMode.toPlayMode() - if(newPlayMode != currentPlayMode) { + if (newPlayMode != currentPlayMode) { newPlayer?.setPlayMode(newPlayMode!!) } else { mutableUiState.update { diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt b/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt index ed041c5..b9ef5ea 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt @@ -4,7 +4,6 @@ import android.os.Bundle import androidx.media3.common.Player import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.ui.ContentScale @@ -62,7 +61,7 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel { println("dummy impl") } - override fun brightnessChange(changeRate: Float, currentValue: Float) { + override fun brightnessChange(changeRate: Float, systemBrightness: Float) { println("dummy impl") } @@ -70,6 +69,10 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel { println("dummy impl") } + override fun onReportEmbeddedConfig(embeddedUiConfig: EmbeddedUiConfig?) { + println("dummy impl") + } + override fun pause() { println("dummy pause") } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerLoadingPlaceholder.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerLoadingPlaceholder.kt index 6f56057..d421e34 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerLoadingPlaceholder.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerLoadingPlaceholder.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -25,13 +26,15 @@ fun VideoPlayerLoadingPlaceholder(aspectRatio: Float = 3F / 1F) { .aspectRatio(aspectRatio), color = Color.Black ) { - Box(contentAlignment = Alignment.Center) { - CircularProgressIndicator(modifier = Modifier - .width(64.dp) - .height(64.dp) - .align((Alignment.Center))) + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator( + modifier = Modifier + .width(64.dp) + .height(64.dp) + .align(Alignment.Center), + color = MaterialTheme.colorScheme.onSurface + ) } - } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt index 76fb6a3..c3f08bd 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -52,6 +53,7 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.media3.common.Player +import net.newpipe.newplayer.model.EmbeddedUiConfig import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.ui.theme.VideoPlayerTheme @@ -86,21 +88,65 @@ fun VideoPlayerUI( val lifecycleOwner = LocalLifecycleOwner.current + val defaultBrightness = getDefaultBrightness(activity) + val screenOrientation = activity.requestedOrientation + // Setup fullscreen - if (uiState.uiMode.fullscreen) { - LaunchedEffect(key1 = true) { - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false + + var embeddedUiConfig: EmbeddedUiConfig? by rememberSaveable { + mutableStateOf(null) + } + + LaunchedEffect(uiState.uiMode.fullscreen) { + if (uiState.uiMode.fullscreen) { + viewModel.onReportEmbeddedConfig( + EmbeddedUiConfig( + WindowCompat.getInsetsController( + window, + view + ).isAppearanceLightStatusBars, + defaultBrightness, + screenOrientation, + ) + ) + } else { + viewModel.onReportEmbeddedConfig(null) } } - // Setup immersive mode - if (uiState.uiMode.systemUiVisible) { - LaunchedEffect(key1 = false) { + LaunchedEffect(uiState.uiMode.fullscreen) { + if (uiState.uiMode.fullscreen) { + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = + false + } else { + uiState.embeddedUiConfig?.let { + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = + it.systemBarInLightMode + } + } + } + + // setup immersive mode + LaunchedEffect( + key1 = uiState.uiMode.controllerUiVisible, + key2 = uiState.uiMode.fullscreen + ) { + if (uiState.uiMode.fullscreen && !uiState.uiMode.systemUiVisible) { + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + } else { windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) } + } + + if (uiState.uiMode.fullscreen) { + if (uiState.contentRatio < 1) { + LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + } else { + LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + } } else { - LaunchedEffect(key1 = true) { - windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + uiState.embeddedUiConfig?.let { + LockScreenOrientation(orientation = it.screenOrientation) } } @@ -116,20 +162,11 @@ fun VideoPlayerUI( } } - // Set Screen Rotation - if (uiState.uiMode.fullscreen) { - if (uiState.contentRatio < 1) { - LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) - } else { - LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) - } - } val displayMetrics = activity.resources.displayMetrics val screenRatio = displayMetrics.widthPixels.toFloat() / displayMetrics.heightPixels.toFloat() - val defaultBrightness = getDefaultBrightness(activity) LaunchedEffect(key1 = uiState.brightness) { Log.d(TAG, "New Brightnes: ${uiState.brightness}") diff --git a/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt b/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt index 3fbf21a..c04be3a 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt @@ -27,6 +27,7 @@ import android.content.ContextWrapper import android.view.WindowManager import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -36,14 +37,9 @@ import java.util.Locale @Composable fun LockScreenOrientation(orientation: Int) { val context = LocalContext.current - DisposableEffect(orientation) { - val activity = context.findActivity() ?: return@DisposableEffect onDispose {} - val originalOrientation = activity.requestedOrientation + LaunchedEffect(orientation) { + val activity = context.findActivity() ?: return@LaunchedEffect activity.requestedOrientation = orientation - onDispose { - // restore original orientation when view disappears - activity.requestedOrientation = originalOrientation - } } }