diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ef1e53d..9a1933b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt b/app/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt index 1b27efa..35ddcb4 100644 --- a/app/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt +++ b/app/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt @@ -1,17 +1,61 @@ +/* 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.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 + fun play() + fun pause() + fun prevStream() + fun nextStream() + fun switchToFullscreen() + fun switchToEmbeddedView() } @HiltViewModel @@ -23,11 +67,51 @@ class VideoPlayerViewModelImpl @Inject constructor( val app = getApplication() + 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") + } } } } \ No newline at end of file diff --git a/app/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt b/app/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt index 964a3ab..da07439 100644 --- a/app/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt +++ b/app/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt @@ -18,6 +18,7 @@ * along with NewPlayer. If not, see . */ + 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 = {}) } } } \ No newline at end of file diff --git a/app/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt b/app/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt index 28ecf86..df1a23c 100644 --- a/app/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt +++ b/app/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt @@ -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() ) { + 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 + ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a5f22c3..05cb7ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,7 +32,7 @@ Previous stream Play Pause - Fullscreen + Toggle fullscreen Chapter selection Playlist item selection \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0373e9b..d2f1d56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }