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.hilt.android)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.foundation) implementation(libs.androidx.foundation)
implementation(libs.androidx.runtime.livedata)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler) ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.navigation.compose) 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 package net.newpipe.newplayer.model
import android.app.Application import android.app.Application
@ -6,12 +26,36 @@ import androidx.lifecycle.SavedStateHandle
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import javax.inject.Inject 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 { interface VideoPlayerViewModel {
val player: Player? val player: Player?
val uiState: StateFlow<VideoPlayerUIState>
fun play()
fun pause()
fun prevStream()
fun nextStream()
fun switchToFullscreen()
fun switchToEmbeddedView()
} }
@HiltViewModel @HiltViewModel
@ -23,11 +67,51 @@ class VideoPlayerViewModelImpl @Inject constructor(
val app = getApplication<Application>() val app = getApplication<Application>()
private val mutableUiState = MutableStateFlow(
VideoPlayerUIState.DEFAULT
)
override val uiState = mutableUiState.asStateFlow()
init { init {
player.prepare() player.prepare()
player.setMediaItem(MediaItem.fromUri(app.getString(R.string.ccc_6502_video))) 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() { override fun onCleared() {
player.release() player.release()
} }
@ -35,6 +119,30 @@ class VideoPlayerViewModelImpl @Inject constructor(
companion object { companion object {
val dummy = object : VideoPlayerViewModel { val dummy = object : VideoPlayerViewModel {
override val player = null 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/>. * along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/ */
package net.newpipe.newplayer.ui package net.newpipe.newplayer.ui
import android.content.pm.ActivityInfo 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.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.FitScreen import androidx.compose.material.icons.filled.FitScreen
import androidx.compose.material.icons.filled.Fullscreen 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.Language
import androidx.compose.material.icons.filled.MoreVert 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.PlayArrow
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.SkipNext 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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 import net.newpipe.newplayer.ui.theme.video_player_onSurface
@Composable @Composable
fun VideoPlayerControllerUI() { fun VideoPlayerControllerUI(
isPlaying: Boolean,
isFullscreen: Boolean,
play: () -> Unit,
pause: () -> Unit,
prevStream: () -> Unit,
nextStream: () -> Unit,
switchToFullscreen: () -> Unit,
switchToEmbeddedView: () -> Unit
) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), color = Color.Transparent modifier = Modifier.fillMaxSize(), color = Color.Transparent
) { ) {
@ -90,13 +101,23 @@ fun VideoPlayerControllerUI() {
.defaultMinSize(minHeight = 45.dp) .defaultMinSize(minHeight = 45.dp)
.padding(top = 4.dp, start = 16.dp, end = 16.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( BottomUI(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.padding(start = 16.dp, end = 16.dp) .padding(start = 16.dp, end = 16.dp)
.defaultMinSize(minHeight = 40.dp) .defaultMinSize(minHeight = 40.dp)
.fillMaxWidth() .fillMaxWidth(),
isFullscreen = isFullscreen,
switchToFullscreen,
switchToEmbeddedView
) )
} }
} }
@ -241,28 +262,45 @@ private fun MainMenu() {
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
@Composable @Composable
private fun CenterUI(modifier: Modifier) { private fun CenterUI(
modifier: Modifier,
isPlaying: Boolean,
play: () -> Unit,
pause: () -> Unit,
nextStream: () -> Unit,
prevStream: () -> Unit
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier modifier = modifier,
) { ) {
CenterControllButton(buttonModifier = Modifier.size(80.dp), CenterControllButton(
buttonModifier = Modifier.size(80.dp),
iconModifier = Modifier.size(40.dp), iconModifier = Modifier.size(40.dp),
icon = Icons.Filled.SkipPrevious, icon = Icons.Filled.SkipPrevious,
contentDescriptoion = stringResource(R.string.widget_description_previous_stream), 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), iconModifier = Modifier.size(60.dp),
icon = Icons.Filled.PlayArrow, icon = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescriptoion = stringResource(R.string.widget_description_play), contentDescriptoion = stringResource(
onClick = {}) if (isPlaying) R.string.widget_description_pause
CenterControllButton(buttonModifier = Modifier.size(80.dp), else R.string.widget_description_play
),
onClick = if (isPlaying) pause else play
)
CenterControllButton(
buttonModifier = Modifier.size(80.dp),
iconModifier = Modifier.size(40.dp), iconModifier = Modifier.size(40.dp),
icon = Icons.Filled.SkipNext, icon = Icons.Filled.SkipNext,
contentDescriptoion = stringResource(R.string.widget_description_next_stream), contentDescriptoion = stringResource(R.string.widget_description_next_stream),
onClick = {}) onClick = nextStream
)
} }
} }
@ -293,8 +331,12 @@ private fun CenterControllButton(
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
@Composable @Composable
private fun BottomUI(modifier: Modifier) { private fun BottomUI(
var isFullscreen: Boolean by rememberSaveable { mutableStateOf(false) } modifier: Modifier,
isFullscreen: Boolean,
switchToFullscreen: () -> Unit,
switchToEmbeddedView: () -> Unit
) {
if (isFullscreen) { if (isFullscreen) {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
@ -308,10 +350,11 @@ private fun BottomUI(modifier: Modifier) {
Text("00:06:45") Text("00:06:45")
Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F)) Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F))
Text("00:09:40") Text("00:09:40")
IconButton(onClick = { isFullscreen = !isFullscreen }) { IconButton(onClick = if(isFullscreen) switchToEmbeddedView else switchToFullscreen) {
Icon( Icon(
imageVector = Icons.Filled.Fullscreen, imageVector = if (isFullscreen) Icons.Filled.FullscreenExit
contentDescription = stringResource(R.string.widget_description_fullscreen) else Icons.Filled.Fullscreen,
contentDescription = stringResource(R.string.widget_description_toggle_fullscreen)
) )
} }
} }
@ -342,7 +385,14 @@ fun PreviewBackgroundSurface(
fun VideoPlayerControllerUIPreviewEmbeded() { fun VideoPlayerControllerUIPreviewEmbeded() {
VideoPlayerTheme { VideoPlayerTheme {
PreviewBackgroundSurface { PreviewBackgroundSurface {
VideoPlayerControllerUI() VideoPlayerControllerUI(isPlaying = false,
isFullscreen = false,
play = {},
pause = {},
prevStream = {},
nextStream = {},
switchToFullscreen = {},
switchToEmbeddedView = {})
} }
} }
} }
@ -352,7 +402,14 @@ fun VideoPlayerControllerUIPreviewEmbeded() {
fun VideoPlayerControllerUIPreviewLandscape() { fun VideoPlayerControllerUIPreviewLandscape() {
VideoPlayerTheme { VideoPlayerTheme {
PreviewBackgroundSurface { PreviewBackgroundSurface {
VideoPlayerControllerUI() VideoPlayerControllerUI(isPlaying = true,
isFullscreen = true,
play = {},
pause = {},
prevStream = {},
nextStream = {},
switchToEmbeddedView = {},
switchToFullscreen = {})
} }
} }
} }
@ -362,7 +419,15 @@ fun VideoPlayerControllerUIPreviewLandscape() {
fun VideoPlayerControllerUIPreviewPortrait() { fun VideoPlayerControllerUIPreviewPortrait() {
VideoPlayerTheme { VideoPlayerTheme {
PreviewBackgroundSurface { 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.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -47,9 +48,12 @@ fun VideoPlayerUI(
viewModel: VideoPlayerViewModel = hiltViewModel<VideoPlayerViewModelImpl>() viewModel: VideoPlayerViewModel = hiltViewModel<VideoPlayerViewModelImpl>()
) { ) {
val uiState by viewModel.uiState.collectAsState()
var lifecycle by remember { var lifecycle by remember {
mutableStateOf(Lifecycle.Event.ON_CREATE) mutableStateOf(Lifecycle.Event.ON_CREATE)
} }
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) { DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
@ -76,22 +80,32 @@ fun VideoPlayerUI(
}, update = { }, update = {
when (lifecycle) { when (lifecycle) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
it.onPause()
viewModel.pause()
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
it.onResume()
} }
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
it.player?.play() viewModel.play()
} }
else -> Unit 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_next_stream">Previous stream</string>
<string name="widget_description_play">Play</string> <string name="widget_description_play">Play</string>
<string name="widget_description_pause">Pause</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_description_chapter_selection">Chapter selection</string>
<string name="widget_descriptoin_playlist_item_selection">Playlist item selection</string> <string name="widget_descriptoin_playlist_item_selection">Playlist item selection</string>
</resources> </resources>

View File

@ -37,6 +37,7 @@ hiltCompiler = "1.2.0"
hiltNavigationCompose = "1.2.0" hiltNavigationCompose = "1.2.0"
lifecycleViewmodelCompose = "2.8.3" lifecycleViewmodelCompose = "2.8.3"
kspVersion = "1.9.0-1.0.13" kspVersion = "1.9.0-1.0.13"
runtimeLivedata = "1.7.0-beta04"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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" } 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-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-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "uiTooling" }
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }