diff --git a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt index 356c14f..7b897c7 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import net.newpipe.newplayer.model.UIModeState import net.newpipe.newplayer.playerInternals.PlaylistItem import net.newpipe.newplayer.playerInternals.fetchPlaylistItem import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromExoplayer @@ -47,6 +48,7 @@ import kotlin.Exception import kotlin.random.Random enum class PlayMode { + IDLE, EMBEDDED_VIDEO, FULLSCREEN_VIDEO, PIP, @@ -73,7 +75,7 @@ interface NewPlayer { val sharingLinkWithOffsetPossible: Boolean var currentPosition: Long var fastSeekAmountSec: Int - val playBackMode: MutableStateFlow + val playBackMode: MutableStateFlow var shuffle: Boolean var repeatMode: RepeatMode @@ -162,7 +164,7 @@ class NewPlayerImpl( private var playerScope = CoroutineScope(Dispatchers.Main + Job()) - override var playBackMode = MutableStateFlow(null) + override var playBackMode = MutableStateFlow(PlayMode.IDLE) override var shuffle: Boolean get() = internalPlayer.shuffleModeEnabled @@ -281,6 +283,11 @@ class NewPlayerImpl( } private fun updatePlaylistItems() { + if (internalPlayer.mediaItemCount == 0) { + playBackMode.update { + PlayMode.IDLE + } + } playerScope.launch { val playlist = getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup) 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 d2c9607..bd9e8e8 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 @@ -57,7 +57,7 @@ enum class UIModeState { val isStreamSelect: Boolean get() = - when(this) { + when (this) { EMBEDDED_VIDEO_STREAM_SELECT -> true FULLSCREEN_VIDEO_STREAM_SELECT -> true else -> false @@ -65,7 +65,7 @@ enum class UIModeState { val isChapterSelect: Boolean get() = - when(this) { + when (this) { EMBEDDED_VIDEO_CHAPTER_SELECT -> true FULLSCREEN_VIDEO_CHAPTER_SELECT -> true else -> false @@ -73,14 +73,14 @@ enum class UIModeState { val systemInsetsVisible: Boolean get() = - when(this) { + when (this) { FULLSCREEN_VIDEO -> false else -> true } val fitScreenRotation: Boolean get() = - when(this) { + when (this) { FULLSCREEN_VIDEO -> true FULLSCREEN_VIDEO_CONTROLLER_UI -> true FULLSCREEN_VIDEO_CHAPTER_SELECT -> true @@ -141,8 +141,8 @@ enum class UIModeState { else -> this } - fun toPlayMode() = when(this) { - PLACEHOLDER -> null + fun toPlayMode() = when (this) { + PLACEHOLDER -> PlayMode.IDLE EMBEDDED_VIDEO -> PlayMode.EMBEDDED_VIDEO EMBEDDED_VIDEO_CONTROLLER_UI -> PlayMode.EMBEDDED_VIDEO EMBEDDED_VIDEO_CHAPTER_SELECT -> PlayMode.EMBEDDED_VIDEO @@ -153,7 +153,7 @@ enum class UIModeState { FULLSCREEN_VIDEO_STREAM_SELECT -> PlayMode.FULLSCREEN_VIDEO } - fun getNextModeWhenBackPressed() = when(this) { + fun getNextModeWhenBackPressed() = when (this) { EMBEDDED_VIDEO_CHAPTER_SELECT -> EMBEDDED_VIDEO EMBEDDED_VIDEO_STREAM_SELECT -> EMBEDDED_VIDEO FULLSCREEN_VIDEO -> EMBEDDED_VIDEO @@ -164,16 +164,14 @@ enum class UIModeState { } companion object { - fun fromPlayMode(playMode: PlayMode?) = - if (playMode != null) - when (playMode) { - PlayMode.EMBEDDED_VIDEO -> EMBEDDED_VIDEO - PlayMode.FULLSCREEN_VIDEO -> FULLSCREEN_VIDEO - PlayMode.PIP -> TODO() - PlayMode.BACKGROUND -> TODO() - PlayMode.AUDIO_FOREGROUND -> TODO() - } - else PLACEHOLDER - + fun fromPlayMode(playMode: PlayMode) = + when (playMode) { + PlayMode.IDLE -> PLACEHOLDER + PlayMode.EMBEDDED_VIDEO -> EMBEDDED_VIDEO + PlayMode.FULLSCREEN_VIDEO -> FULLSCREEN_VIDEO + PlayMode.PIP -> TODO() + PlayMode.BACKGROUND -> TODO() + PlayMode.AUDIO_FOREGROUND -> TODO() + } } } \ 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 ef64481..23d6ef5 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 @@ -145,6 +145,11 @@ class VideoPlayerViewModelImpl @Inject constructor( mutableUiState.update { it.copy(playing = isPlaying, isLoading = false) } + if(isPlaying && uiState.value.uiMode.controllerUiVisible) { + resetHideUiDelayedJob() + } else { + uiVisibilityJob?.cancel() + } } override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) { @@ -181,6 +186,7 @@ class VideoPlayerViewModelImpl @Inject constructor( viewModelScope.launch { newPlayer.playBackMode.collect { newMode -> val currentMode = mutableUiState.value.uiMode.toPlayMode() + println("gurken mode: $currentMode newMode: $newMode") if (currentMode != newMode) { mutableUiState.update { it.copy( 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 bd16870..2598ae8 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 @@ -55,6 +55,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.media3.common.Player import net.newpipe.newplayer.model.EmbeddedUiConfig +import net.newpipe.newplayer.model.UIModeState import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.ui.theme.VideoPlayerTheme @@ -116,10 +117,10 @@ fun VideoPlayerUI( LaunchedEffect( key1 = uiState.uiMode.systemInsetsVisible, ) { - if (uiState.uiMode.fullscreen && !uiState.uiMode.systemInsetsVisible) { - windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) - } else { + if (uiState.uiMode.systemInsetsVisible) { windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) + } else { + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) } } @@ -161,37 +162,45 @@ fun VideoPlayerUI( } // Set UI - Surface( - modifier = Modifier.then( - if (uiState.uiMode.fullscreen) Modifier.fillMaxSize() - else Modifier - .fillMaxWidth() - .aspectRatio(uiState.embeddedUiRatio) - ), color = Color.Black - ) { - Box(contentAlignment = Alignment.Center) { - PlaySurface( - player = viewModel.newPlayer?.internalPlayer, - lifecycle = lifecycle, - fitMode = uiState.contentFitMode, - uiRatio = if (uiState.uiMode.fullscreen) screenRatio - else uiState.embeddedUiRatio, - contentRatio = uiState.contentRatio + if (uiState.uiMode == UIModeState.PLACEHOLDER) { + VideoPlayerLoadingPlaceholder(uiState.embeddedUiRatio) + } else { + Surface( + modifier = Modifier.then( + if (uiState.uiMode.fullscreen) Modifier.fillMaxSize() + else Modifier + .fillMaxWidth() + .aspectRatio(uiState.embeddedUiRatio) + ), color = Color.Black + ) { + Box(contentAlignment = Alignment.Center) { + PlaySurface( + player = viewModel.newPlayer?.internalPlayer, + lifecycle = lifecycle, + fitMode = uiState.contentFitMode, + uiRatio = if (uiState.uiMode.fullscreen) screenRatio + else uiState.embeddedUiRatio, + contentRatio = uiState.contentRatio + ) + } + + // the checks if VideoPlayerControllerUI should be visible or not are done by + // The VideoPlayerControllerUI composable itself. This is because Visibility of + // the controller is more complicated than just using a simple if statement. + VideoPlayerControllerUI( + viewModel, uiState = uiState ) - } - // the checks if VideoPlayerControllerUI should be visible or not are done by - // The VideoPlayerControllerUI composable itself. This is because Visibility of - // the controller is more complicated than just using a simple if statement. - VideoPlayerControllerUI( - viewModel, uiState = uiState - ) - - AnimatedVisibility(visible = uiState.uiMode.isStreamSelect) { - StreamSelectUI(viewModel = viewModel, uiState = uiState, isChapterSelect = false) - } - AnimatedVisibility(visible = uiState.uiMode.isChapterSelect) { - StreamSelectUI(viewModel = viewModel, uiState = uiState, isChapterSelect = true) + AnimatedVisibility(visible = uiState.uiMode.isStreamSelect) { + StreamSelectUI( + viewModel = viewModel, + uiState = uiState, + isChapterSelect = false + ) + } + AnimatedVisibility(visible = uiState.uiMode.isChapterSelect) { + StreamSelectUI(viewModel = viewModel, uiState = uiState, isChapterSelect = true) + } } } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt index 7e83ba2..806a754 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt @@ -157,7 +157,10 @@ fun ReorderableStreamItemsList( haptic = haptic, onDragFinished = viewModel::onStreamItemDragFinished, isDragging = isDragging, - isCurrentlyPlaying = playlistItem.uniqueId == uiState.currentlyPlaying.uniqueId + isCurrentlyPlaying = playlistItem.uniqueId == uiState.currentlyPlaying.uniqueId, + onDelete = { + viewModel.removePlaylistItem(index) + } ) } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt index 554b842..a11ba3b 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt @@ -21,6 +21,7 @@ package net.newpipe.newplayer.ui.videoplayer.streamselect import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -42,12 +43,17 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.DragHandle +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -78,12 +84,14 @@ import net.newpipe.newplayer.utils.getLocale import net.newpipe.newplayer.utils.getTimeStringFromMs import sh.calvin.reorderable.ReorderableCollectionItemScope +@OptIn(ExperimentalMaterial3Api::class) @Composable fun StreamItem( modifier: Modifier = Modifier, playlistItem: PlaylistItem, onClicked: (Long) -> Unit, onDragFinished: () -> Unit, + onDelete: () -> Unit, reorderableScope: ReorderableCollectionItemScope?, haptic: ReorderHapticFeedback?, isDragging: Boolean, @@ -92,128 +100,151 @@ fun StreamItem( val locale = getLocale()!! val interactionSource = remember { MutableInteractionSource() } - Box(modifier = modifier - .height(60.dp) - .clip(ITEM_CORNER_SHAPE) - .clickable { - onClicked(playlistItem.uniqueId) - }) { - AnimatedVisibility( - visible = isDragging, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(400)) - ) { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - shadowElevation = 8.dp, - shape = ITEM_CORNER_SHAPE - ) {} + val dismissState = rememberSwipeToDismissBoxState(confirmValueChange = { + when (it) { + SwipeToDismissBoxValue.StartToEnd -> { + onDelete() + true + } + + SwipeToDismissBoxValue.EndToStart -> { + onDelete() + true + } + + SwipeToDismissBoxValue.Settled -> true } + }) - AnimatedVisibility( - visible = isCurrentlyPlaying, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(400)) - ) { - Surface( - modifier = Modifier.fillMaxSize(), - color = Color.White.copy(alpha = 0.2f), - ) {} - } + SwipeToDismissBox( + modifier = modifier.height(60.dp), state = dismissState, + backgroundContent = { + }, + ) { + Box(modifier = Modifier + .fillMaxSize() + .clip(ITEM_CORNER_SHAPE) + .clickable { + onClicked(playlistItem.uniqueId) + }) { - Row( - modifier = modifier - .padding(0.dp) - ) { - Box( - modifier = Modifier - .aspectRatio(16f / 9f) - .fillMaxSize() + androidx.compose.animation.AnimatedVisibility( + visible = isDragging, + enter = fadeIn(animationSpec = tween(200)), + exit = fadeOut(animationSpec = tween(400)) ) { - val contentDescription = stringResource(R.string.stream_item_thumbnail) - Thumbnail( - modifier = Modifier.fillMaxHeight(), - thumbnail = playlistItem.thumbnail, - contentDescription = contentDescription, - shape = ITEM_CORNER_SHAPE - ) Surface( - color = CONTROLLER_UI_BACKGROUND_COLOR, - shape = ITEM_CORNER_SHAPE, + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + shadowElevation = 8.dp, + shape = ITEM_CORNER_SHAPE + ) {} + } + + androidx.compose.animation.AnimatedVisibility( + visible = isCurrentlyPlaying, + enter = fadeIn(animationSpec = tween(200)), + exit = fadeOut(animationSpec = tween(400)) + ) { + Surface( + modifier = Modifier.fillMaxSize(), + color = Color.White.copy(alpha = 0.2f), + ) {} + } + + Row( + modifier = modifier + .padding(0.dp) + ) { + Box( modifier = Modifier - .wrapContentSize() - .align(Alignment.BottomEnd) - .padding(4.dp) + .aspectRatio(16f / 9f) + .fillMaxSize() + ) { + val contentDescription = stringResource(R.string.stream_item_thumbnail) + Thumbnail( + modifier = Modifier.fillMaxHeight(), + thumbnail = playlistItem.thumbnail, + contentDescription = contentDescription, + shape = ITEM_CORNER_SHAPE + ) + Surface( + color = CONTROLLER_UI_BACKGROUND_COLOR, + shape = ITEM_CORNER_SHAPE, + modifier = Modifier + .wrapContentSize() + .align(Alignment.BottomEnd) + .padding(4.dp) + ) { + Text( + modifier = Modifier.padding( + start = 4.dp, + end = 4.dp, + top = 0.5.dp, + bottom = 0.5.dp + ), + text = getTimeStringFromMs( + playlistItem.lengthInS * 1000L, + locale, + leadingZerosForMinutes = false + ), + fontSize = 14.sp, + ) + } + } + + Column( + modifier = Modifier + .padding(6.dp) + .weight(1f) + .wrapContentHeight() + .fillMaxWidth() ) { Text( - modifier = Modifier.padding( - start = 4.dp, - end = 4.dp, - top = 0.5.dp, - bottom = 0.5.dp - ), - text = getTimeStringFromMs( - playlistItem.lengthInS * 1000L, - locale, - leadingZerosForMinutes = false - ), + text = playlistItem.title, fontSize = 14.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = playlistItem.creator, + fontSize = 13.sp, + fontWeight = FontWeight.Light, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } - } - - Column( - modifier = Modifier - .padding(6.dp) - .weight(1f) - .wrapContentHeight() - .fillMaxWidth() - ) { - Text( - text = playlistItem.title, - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = playlistItem.creator, - fontSize = 13.sp, - fontWeight = FontWeight.Light, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - IconButton( - modifier = if (reorderableScope != null) { - with(reorderableScope) { + IconButton( + modifier = if (reorderableScope != null) { + with(reorderableScope) { + Modifier + .aspectRatio(1f) + .fillMaxSize() + .draggableHandle( + onDragStarted = { + haptic?.performHapticFeedback(ReorderHapticFeedbackType.START) + }, + onDragStopped = { + haptic?.performHapticFeedback(ReorderHapticFeedbackType.END) + onDragFinished() + }, + interactionSource = interactionSource, + ) + } + } else { Modifier .aspectRatio(1f) .fillMaxSize() - .draggableHandle( - onDragStarted = { - haptic?.performHapticFeedback(ReorderHapticFeedbackType.START) - }, - onDragStopped = { - haptic?.performHapticFeedback(ReorderHapticFeedbackType.END) - onDragFinished() - }, - interactionSource = interactionSource, - ) - } - } else { - Modifier - .aspectRatio(1f) - .fillMaxSize() - }, - onClick = {} - ) { - Icon( - imageVector = Icons.Filled.DragHandle, - contentDescription = stringResource(R.string.stream_item_drag_handle) - ) + }, + onClick = {} + ) { + Icon( + imageVector = Icons.Filled.DragHandle, + contentDescription = stringResource(R.string.stream_item_drag_handle) + ) + } } } } @@ -233,7 +264,8 @@ fun StreamItemPreview() { haptic = null, onDragFinished = {}, isDragging = false, - isCurrentlyPlaying = true + isCurrentlyPlaying = true, + onDelete = {println("has ben deleted")} ) } }