get basic ui functions interact with player

This commit is contained in:
Christian Schabesberger 2024-07-10 15:21:30 +02:00
parent 1c01609af9
commit e1d8447304
6 changed files with 218 additions and 28 deletions

View File

@ -83,6 +83,7 @@ dependencies {
implementation(libs.hilt.android)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.foundation)
implementation(libs.androidx.runtime.livedata)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.navigation.compose)

View File

@ -1,3 +1,23 @@
/* 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
@ -6,12 +26,36 @@ import androidx.lifecycle.SavedStateHandle
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import net.newpipe.newplayer.R
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
data class VideoPlayerUIState(
var fullscreen: Boolean,
var playing: Boolean,
var uiVissible: Boolean
){
companion object {
val DEFAULT = VideoPlayerUIState(
fullscreen = false,
playing = false,
uiVissible = false
)
}
}
interface VideoPlayerViewModel {
val player: Player?
val uiState: StateFlow<VideoPlayerUIState>
fun play()
fun pause()
fun prevStream()
fun nextStream()
fun switchToFullscreen()
fun switchToEmbeddedView()
}
@HiltViewModel
@ -23,11 +67,51 @@ class VideoPlayerViewModelImpl @Inject constructor(
val app = getApplication<Application>()
private val mutableUiState = MutableStateFlow(
VideoPlayerUIState.DEFAULT
)
override val uiState = mutableUiState.asStateFlow()
init {
player.prepare()
player.setMediaItem(MediaItem.fromUri(app.getString(R.string.ccc_6502_video)))
}
override fun play() {
player.play()
mutableUiState.update {
it.copy(playing = true)
}
}
override fun pause() {
player.pause()
mutableUiState.update {
it.copy(playing = false)
}
}
override fun prevStream() {
println("imeplement prev stream")
}
override fun nextStream() {
println("implement next stream")
}
override fun switchToEmbeddedView() {
mutableUiState.update {
it.copy(fullscreen = false)
}
}
override fun switchToFullscreen() {
mutableUiState.update {
it.copy(fullscreen = true)
}
}
override fun onCleared() {
player.release()
}
@ -35,6 +119,30 @@ class VideoPlayerViewModelImpl @Inject constructor(
companion object {
val dummy = object : VideoPlayerViewModel {
override val player = null
override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT)
override fun play() {
println("dummy impl")
}
override fun switchToEmbeddedView() {
println("dummy impl")
}
override fun switchToFullscreen() {
println("dummy impl")
}
override fun pause() {
println("dummy pause")
}
override fun prevStream() {
println("dummy impl")
}
override fun nextStream() {
println("dummy impl")
}
}
}
}

View File

@ -18,6 +18,7 @@
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer.ui
import android.content.pm.ActivityInfo
@ -38,8 +39,10 @@ import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.FitScreen
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.FullscreenExit
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.SkipNext
@ -59,7 +62,6 @@ import androidx.compose.runtime.Composable
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
@ -78,7 +80,16 @@ import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.theme.video_player_onSurface
@Composable
fun VideoPlayerControllerUI() {
fun VideoPlayerControllerUI(
isPlaying: Boolean,
isFullscreen: Boolean,
play: () -> Unit,
pause: () -> Unit,
prevStream: () -> Unit,
nextStream: () -> Unit,
switchToFullscreen: () -> Unit,
switchToEmbeddedView: () -> Unit
) {
Surface(
modifier = Modifier.fillMaxSize(), color = Color.Transparent
) {
@ -90,13 +101,23 @@ fun VideoPlayerControllerUI() {
.defaultMinSize(minHeight = 45.dp)
.padding(top = 4.dp, start = 16.dp, end = 16.dp)
)
CenterUI(modifier = Modifier.align(Alignment.Center))
CenterUI(
modifier = Modifier.align(Alignment.Center),
isPlaying,
play = play,
pause = pause,
prevStream = prevStream,
nextStream = nextStream
)
BottomUI(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 16.dp, end = 16.dp)
.defaultMinSize(minHeight = 40.dp)
.fillMaxWidth()
.fillMaxWidth(),
isFullscreen = isFullscreen,
switchToFullscreen,
switchToEmbeddedView
)
}
}
@ -241,28 +262,45 @@ private fun MainMenu() {
///////////////////////////////////////////////////////////////////
@Composable
private fun CenterUI(modifier: Modifier) {
private fun CenterUI(
modifier: Modifier,
isPlaying: Boolean,
play: () -> Unit,
pause: () -> Unit,
nextStream: () -> Unit,
prevStream: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier
modifier = modifier,
) {
CenterControllButton(buttonModifier = Modifier.size(80.dp),
CenterControllButton(
buttonModifier = Modifier.size(80.dp),
iconModifier = Modifier.size(40.dp),
icon = Icons.Filled.SkipPrevious,
contentDescriptoion = stringResource(R.string.widget_description_previous_stream),
onClick = {})
onClick = prevStream
)
CenterControllButton(buttonModifier = Modifier.size(80.dp),
CenterControllButton(
buttonModifier = Modifier.size(80.dp),
iconModifier = Modifier.size(60.dp),
icon = Icons.Filled.PlayArrow,
contentDescriptoion = stringResource(R.string.widget_description_play),
onClick = {})
CenterControllButton(buttonModifier = Modifier.size(80.dp),
icon = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescriptoion = stringResource(
if (isPlaying) R.string.widget_description_pause
else R.string.widget_description_play
),
onClick = if (isPlaying) pause else play
)
CenterControllButton(
buttonModifier = Modifier.size(80.dp),
iconModifier = Modifier.size(40.dp),
icon = Icons.Filled.SkipNext,
contentDescriptoion = stringResource(R.string.widget_description_next_stream),
onClick = {})
onClick = nextStream
)
}
}
@ -293,8 +331,12 @@ private fun CenterControllButton(
///////////////////////////////////////////////////////////////////
@Composable
private fun BottomUI(modifier: Modifier) {
var isFullscreen: Boolean by rememberSaveable { mutableStateOf(false) }
private fun BottomUI(
modifier: Modifier,
isFullscreen: Boolean,
switchToFullscreen: () -> Unit,
switchToEmbeddedView: () -> Unit
) {
if (isFullscreen) {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
@ -308,10 +350,11 @@ private fun BottomUI(modifier: Modifier) {
Text("00:06:45")
Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F))
Text("00:09:40")
IconButton(onClick = { isFullscreen = !isFullscreen }) {
IconButton(onClick = if(isFullscreen) switchToEmbeddedView else switchToFullscreen) {
Icon(
imageVector = Icons.Filled.Fullscreen,
contentDescription = stringResource(R.string.widget_description_fullscreen)
imageVector = if (isFullscreen) Icons.Filled.FullscreenExit
else Icons.Filled.Fullscreen,
contentDescription = stringResource(R.string.widget_description_toggle_fullscreen)
)
}
}
@ -342,7 +385,14 @@ fun PreviewBackgroundSurface(
fun VideoPlayerControllerUIPreviewEmbeded() {
VideoPlayerTheme {
PreviewBackgroundSurface {
VideoPlayerControllerUI()
VideoPlayerControllerUI(isPlaying = false,
isFullscreen = false,
play = {},
pause = {},
prevStream = {},
nextStream = {},
switchToFullscreen = {},
switchToEmbeddedView = {})
}
}
}
@ -352,7 +402,14 @@ fun VideoPlayerControllerUIPreviewEmbeded() {
fun VideoPlayerControllerUIPreviewLandscape() {
VideoPlayerTheme {
PreviewBackgroundSurface {
VideoPlayerControllerUI()
VideoPlayerControllerUI(isPlaying = true,
isFullscreen = true,
play = {},
pause = {},
prevStream = {},
nextStream = {},
switchToEmbeddedView = {},
switchToFullscreen = {})
}
}
}
@ -362,7 +419,15 @@ fun VideoPlayerControllerUIPreviewLandscape() {
fun VideoPlayerControllerUIPreviewPortrait() {
VideoPlayerTheme {
PreviewBackgroundSurface {
VideoPlayerControllerUI()
VideoPlayerControllerUI(
isPlaying = false,
isFullscreen = true,
play = {},
pause = {},
prevStream = {},
nextStream = {},
switchToEmbeddedView = {},
switchToFullscreen = {})
}
}
}

View File

@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -47,9 +48,12 @@ fun VideoPlayerUI(
viewModel: VideoPlayerViewModel = hiltViewModel<VideoPlayerViewModelImpl>()
) {
val uiState by viewModel.uiState.collectAsState()
var lifecycle by remember {
mutableStateOf(Lifecycle.Event.ON_CREATE)
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
@ -76,22 +80,32 @@ fun VideoPlayerUI(
}, update = {
when (lifecycle) {
Lifecycle.Event.ON_PAUSE -> {
it.onPause()
viewModel.pause()
}
Lifecycle.Event.ON_RESUME -> {
it.onResume()
}
Lifecycle.Event.ON_START -> {
it.player?.play()
viewModel.play()
}
else -> Unit
}
})
VideoPlayerControllerUI()
VideoPlayerControllerUI(
isPlaying = viewModel.player?.isPlaying ?: false,
isFullscreen = uiState.fullscreen,
play = viewModel::play,
pause = viewModel::pause,
prevStream = viewModel::prevStream,
nextStream = viewModel::nextStream,
switchToFullscreen = viewModel::switchToFullscreen,
switchToEmbeddedView = viewModel::switchToEmbeddedView
)
}
}

View File

@ -32,7 +32,7 @@
<string name="widget_description_next_stream">Previous stream</string>
<string name="widget_description_play">Play</string>
<string name="widget_description_pause">Pause</string>
<string name="widget_description_fullscreen">Fullscreen</string>
<string name="widget_description_toggle_fullscreen">Toggle fullscreen</string>
<string name="widget_description_chapter_selection">Chapter selection</string>
<string name="widget_descriptoin_playlist_item_selection">Playlist item selection</string>
</resources>

View File

@ -37,6 +37,7 @@ hiltCompiler = "1.2.0"
hiltNavigationCompose = "1.2.0"
lifecycleViewmodelCompose = "2.8.3"
kspVersion = "1.9.0-1.0.13"
runtimeLivedata = "1.7.0-beta04"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -59,6 +60,7 @@ androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navig
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hiltAndroid" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "uiTooling" }
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }