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,17 +1,61 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer.model
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.AndroidViewModel
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" }