make item deletion almost possible

This commit is contained in:
Christian Schabesberger 2024-09-06 20:10:25 +02:00
parent 430bed20b6
commit 3dc5ca3e10
6 changed files with 216 additions and 161 deletions

View File

@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.playerInternals.PlaylistItem import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.playerInternals.fetchPlaylistItem import net.newpipe.newplayer.playerInternals.fetchPlaylistItem
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromExoplayer import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromExoplayer
@ -47,6 +48,7 @@ import kotlin.Exception
import kotlin.random.Random import kotlin.random.Random
enum class PlayMode { enum class PlayMode {
IDLE,
EMBEDDED_VIDEO, EMBEDDED_VIDEO,
FULLSCREEN_VIDEO, FULLSCREEN_VIDEO,
PIP, PIP,
@ -73,7 +75,7 @@ interface NewPlayer {
val sharingLinkWithOffsetPossible: Boolean val sharingLinkWithOffsetPossible: Boolean
var currentPosition: Long var currentPosition: Long
var fastSeekAmountSec: Int var fastSeekAmountSec: Int
val playBackMode: MutableStateFlow<PlayMode?> val playBackMode: MutableStateFlow<PlayMode>
var shuffle: Boolean var shuffle: Boolean
var repeatMode: RepeatMode var repeatMode: RepeatMode
@ -162,7 +164,7 @@ class NewPlayerImpl(
private var playerScope = CoroutineScope(Dispatchers.Main + Job()) private var playerScope = CoroutineScope(Dispatchers.Main + Job())
override var playBackMode = MutableStateFlow<PlayMode?>(null) override var playBackMode = MutableStateFlow(PlayMode.IDLE)
override var shuffle: Boolean override var shuffle: Boolean
get() = internalPlayer.shuffleModeEnabled get() = internalPlayer.shuffleModeEnabled
@ -281,6 +283,11 @@ class NewPlayerImpl(
} }
private fun updatePlaylistItems() { private fun updatePlaylistItems() {
if (internalPlayer.mediaItemCount == 0) {
playBackMode.update {
PlayMode.IDLE
}
}
playerScope.launch { playerScope.launch {
val playlist = val playlist =
getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup) getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup)

View File

@ -57,7 +57,7 @@ enum class UIModeState {
val isStreamSelect: Boolean val isStreamSelect: Boolean
get() = get() =
when(this) { when (this) {
EMBEDDED_VIDEO_STREAM_SELECT -> true EMBEDDED_VIDEO_STREAM_SELECT -> true
FULLSCREEN_VIDEO_STREAM_SELECT -> true FULLSCREEN_VIDEO_STREAM_SELECT -> true
else -> false else -> false
@ -65,7 +65,7 @@ enum class UIModeState {
val isChapterSelect: Boolean val isChapterSelect: Boolean
get() = get() =
when(this) { when (this) {
EMBEDDED_VIDEO_CHAPTER_SELECT -> true EMBEDDED_VIDEO_CHAPTER_SELECT -> true
FULLSCREEN_VIDEO_CHAPTER_SELECT -> true FULLSCREEN_VIDEO_CHAPTER_SELECT -> true
else -> false else -> false
@ -73,14 +73,14 @@ enum class UIModeState {
val systemInsetsVisible: Boolean val systemInsetsVisible: Boolean
get() = get() =
when(this) { when (this) {
FULLSCREEN_VIDEO -> false FULLSCREEN_VIDEO -> false
else -> true else -> true
} }
val fitScreenRotation: Boolean val fitScreenRotation: Boolean
get() = get() =
when(this) { when (this) {
FULLSCREEN_VIDEO -> true FULLSCREEN_VIDEO -> true
FULLSCREEN_VIDEO_CONTROLLER_UI -> true FULLSCREEN_VIDEO_CONTROLLER_UI -> true
FULLSCREEN_VIDEO_CHAPTER_SELECT -> true FULLSCREEN_VIDEO_CHAPTER_SELECT -> true
@ -141,8 +141,8 @@ enum class UIModeState {
else -> this else -> this
} }
fun toPlayMode() = when(this) { fun toPlayMode() = when (this) {
PLACEHOLDER -> null PLACEHOLDER -> PlayMode.IDLE
EMBEDDED_VIDEO -> PlayMode.EMBEDDED_VIDEO EMBEDDED_VIDEO -> PlayMode.EMBEDDED_VIDEO
EMBEDDED_VIDEO_CONTROLLER_UI -> PlayMode.EMBEDDED_VIDEO EMBEDDED_VIDEO_CONTROLLER_UI -> PlayMode.EMBEDDED_VIDEO
EMBEDDED_VIDEO_CHAPTER_SELECT -> PlayMode.EMBEDDED_VIDEO EMBEDDED_VIDEO_CHAPTER_SELECT -> PlayMode.EMBEDDED_VIDEO
@ -153,7 +153,7 @@ enum class UIModeState {
FULLSCREEN_VIDEO_STREAM_SELECT -> PlayMode.FULLSCREEN_VIDEO FULLSCREEN_VIDEO_STREAM_SELECT -> PlayMode.FULLSCREEN_VIDEO
} }
fun getNextModeWhenBackPressed() = when(this) { fun getNextModeWhenBackPressed() = when (this) {
EMBEDDED_VIDEO_CHAPTER_SELECT -> EMBEDDED_VIDEO EMBEDDED_VIDEO_CHAPTER_SELECT -> EMBEDDED_VIDEO
EMBEDDED_VIDEO_STREAM_SELECT -> EMBEDDED_VIDEO EMBEDDED_VIDEO_STREAM_SELECT -> EMBEDDED_VIDEO
FULLSCREEN_VIDEO -> EMBEDDED_VIDEO FULLSCREEN_VIDEO -> EMBEDDED_VIDEO
@ -164,16 +164,14 @@ enum class UIModeState {
} }
companion object { companion object {
fun fromPlayMode(playMode: PlayMode?) = fun fromPlayMode(playMode: PlayMode) =
if (playMode != null) when (playMode) {
when (playMode) { PlayMode.IDLE -> PLACEHOLDER
PlayMode.EMBEDDED_VIDEO -> EMBEDDED_VIDEO PlayMode.EMBEDDED_VIDEO -> EMBEDDED_VIDEO
PlayMode.FULLSCREEN_VIDEO -> FULLSCREEN_VIDEO PlayMode.FULLSCREEN_VIDEO -> FULLSCREEN_VIDEO
PlayMode.PIP -> TODO() PlayMode.PIP -> TODO()
PlayMode.BACKGROUND -> TODO() PlayMode.BACKGROUND -> TODO()
PlayMode.AUDIO_FOREGROUND -> TODO() PlayMode.AUDIO_FOREGROUND -> TODO()
} }
else PLACEHOLDER
} }
} }

View File

@ -145,6 +145,11 @@ class VideoPlayerViewModelImpl @Inject constructor(
mutableUiState.update { mutableUiState.update {
it.copy(playing = isPlaying, isLoading = false) it.copy(playing = isPlaying, isLoading = false)
} }
if(isPlaying && uiState.value.uiMode.controllerUiVisible) {
resetHideUiDelayedJob()
} else {
uiVisibilityJob?.cancel()
}
} }
override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) { override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) {
@ -181,6 +186,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
newPlayer.playBackMode.collect { newMode -> newPlayer.playBackMode.collect { newMode ->
val currentMode = mutableUiState.value.uiMode.toPlayMode() val currentMode = mutableUiState.value.uiMode.toPlayMode()
println("gurken mode: $currentMode newMode: $newMode")
if (currentMode != newMode) { if (currentMode != newMode) {
mutableUiState.update { mutableUiState.update {
it.copy( it.copy(

View File

@ -55,6 +55,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.media3.common.Player import androidx.media3.common.Player
import net.newpipe.newplayer.model.EmbeddedUiConfig import net.newpipe.newplayer.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@ -116,10 +117,10 @@ fun VideoPlayerUI(
LaunchedEffect( LaunchedEffect(
key1 = uiState.uiMode.systemInsetsVisible, key1 = uiState.uiMode.systemInsetsVisible,
) { ) {
if (uiState.uiMode.fullscreen && !uiState.uiMode.systemInsetsVisible) { if (uiState.uiMode.systemInsetsVisible) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
} else {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
} else {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
} }
} }
@ -161,37 +162,45 @@ fun VideoPlayerUI(
} }
// Set UI // Set UI
Surface( if (uiState.uiMode == UIModeState.PLACEHOLDER) {
modifier = Modifier.then( VideoPlayerLoadingPlaceholder(uiState.embeddedUiRatio)
if (uiState.uiMode.fullscreen) Modifier.fillMaxSize() } else {
else Modifier Surface(
.fillMaxWidth() modifier = Modifier.then(
.aspectRatio(uiState.embeddedUiRatio) if (uiState.uiMode.fullscreen) Modifier.fillMaxSize()
), color = Color.Black else Modifier
) { .fillMaxWidth()
Box(contentAlignment = Alignment.Center) { .aspectRatio(uiState.embeddedUiRatio)
PlaySurface( ), color = Color.Black
player = viewModel.newPlayer?.internalPlayer, ) {
lifecycle = lifecycle, Box(contentAlignment = Alignment.Center) {
fitMode = uiState.contentFitMode, PlaySurface(
uiRatio = if (uiState.uiMode.fullscreen) screenRatio player = viewModel.newPlayer?.internalPlayer,
else uiState.embeddedUiRatio, lifecycle = lifecycle,
contentRatio = uiState.contentRatio 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 AnimatedVisibility(visible = uiState.uiMode.isStreamSelect) {
// The VideoPlayerControllerUI composable itself. This is because Visibility of StreamSelectUI(
// the controller is more complicated than just using a simple if statement. viewModel = viewModel,
VideoPlayerControllerUI( uiState = uiState,
viewModel, uiState = uiState isChapterSelect = false
) )
}
AnimatedVisibility(visible = uiState.uiMode.isStreamSelect) { AnimatedVisibility(visible = uiState.uiMode.isChapterSelect) {
StreamSelectUI(viewModel = viewModel, uiState = uiState, isChapterSelect = false) StreamSelectUI(viewModel = viewModel, uiState = uiState, isChapterSelect = true)
} }
AnimatedVisibility(visible = uiState.uiMode.isChapterSelect) {
StreamSelectUI(viewModel = viewModel, uiState = uiState, isChapterSelect = true)
} }
} }
} }

View File

@ -157,7 +157,10 @@ fun ReorderableStreamItemsList(
haptic = haptic, haptic = haptic,
onDragFinished = viewModel::onStreamItemDragFinished, onDragFinished = viewModel::onStreamItemDragFinished,
isDragging = isDragging, isDragging = isDragging,
isCurrentlyPlaying = playlistItem.uniqueId == uiState.currentlyPlaying.uniqueId isCurrentlyPlaying = playlistItem.uniqueId == uiState.currentlyPlaying.uniqueId,
onDelete = {
viewModel.removePlaylistItem(index)
}
) )
} }
} }

View File

@ -21,6 +21,7 @@
package net.newpipe.newplayer.ui.videoplayer.streamselect package net.newpipe.newplayer.ui.videoplayer.streamselect
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -42,12 +43,17 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -78,12 +84,14 @@ import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs import net.newpipe.newplayer.utils.getTimeStringFromMs
import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableCollectionItemScope
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun StreamItem( fun StreamItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
playlistItem: PlaylistItem, playlistItem: PlaylistItem,
onClicked: (Long) -> Unit, onClicked: (Long) -> Unit,
onDragFinished: () -> Unit, onDragFinished: () -> Unit,
onDelete: () -> Unit,
reorderableScope: ReorderableCollectionItemScope?, reorderableScope: ReorderableCollectionItemScope?,
haptic: ReorderHapticFeedback?, haptic: ReorderHapticFeedback?,
isDragging: Boolean, isDragging: Boolean,
@ -92,128 +100,151 @@ fun StreamItem(
val locale = getLocale()!! val locale = getLocale()!!
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box(modifier = modifier
.height(60.dp)
.clip(ITEM_CORNER_SHAPE)
.clickable {
onClicked(playlistItem.uniqueId)
}) {
AnimatedVisibility( val dismissState = rememberSwipeToDismissBoxState(confirmValueChange = {
visible = isDragging, when (it) {
enter = fadeIn(animationSpec = tween(200)), SwipeToDismissBoxValue.StartToEnd -> {
exit = fadeOut(animationSpec = tween(400)) onDelete()
) { true
Surface( }
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background, SwipeToDismissBoxValue.EndToStart -> {
shadowElevation = 8.dp, onDelete()
shape = ITEM_CORNER_SHAPE true
) {} }
SwipeToDismissBoxValue.Settled -> true
} }
})
AnimatedVisibility( SwipeToDismissBox(
visible = isCurrentlyPlaying, modifier = modifier.height(60.dp), state = dismissState,
enter = fadeIn(animationSpec = tween(200)), backgroundContent = {
exit = fadeOut(animationSpec = tween(400)) },
) { ) {
Surface( Box(modifier = Modifier
modifier = Modifier.fillMaxSize(), .fillMaxSize()
color = Color.White.copy(alpha = 0.2f), .clip(ITEM_CORNER_SHAPE)
) {} .clickable {
} onClicked(playlistItem.uniqueId)
}) {
Row( androidx.compose.animation.AnimatedVisibility(
modifier = modifier visible = isDragging,
.padding(0.dp) enter = fadeIn(animationSpec = tween(200)),
) { exit = fadeOut(animationSpec = tween(400))
Box(
modifier = Modifier
.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( Surface(
color = CONTROLLER_UI_BACKGROUND_COLOR, modifier = Modifier.fillMaxSize(),
shape = ITEM_CORNER_SHAPE, 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 modifier = Modifier
.wrapContentSize() .aspectRatio(16f / 9f)
.align(Alignment.BottomEnd) .fillMaxSize()
.padding(4.dp) ) {
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( Text(
modifier = Modifier.padding( text = playlistItem.title,
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, 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) {
Column( with(reorderableScope) {
modifier = Modifier Modifier
.padding(6.dp) .aspectRatio(1f)
.weight(1f) .fillMaxSize()
.wrapContentHeight() .draggableHandle(
.fillMaxWidth() onDragStarted = {
) { haptic?.performHapticFeedback(ReorderHapticFeedbackType.START)
Text( },
text = playlistItem.title, onDragStopped = {
fontSize = 14.sp, haptic?.performHapticFeedback(ReorderHapticFeedbackType.END)
fontWeight = FontWeight.Bold, onDragFinished()
maxLines = 1, },
overflow = TextOverflow.Ellipsis interactionSource = interactionSource,
) )
Text( }
text = playlistItem.creator, } else {
fontSize = 13.sp,
fontWeight = FontWeight.Light,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
IconButton(
modifier = if (reorderableScope != null) {
with(reorderableScope) {
Modifier Modifier
.aspectRatio(1f) .aspectRatio(1f)
.fillMaxSize() .fillMaxSize()
.draggableHandle( },
onDragStarted = { onClick = {}
haptic?.performHapticFeedback(ReorderHapticFeedbackType.START) ) {
}, Icon(
onDragStopped = { imageVector = Icons.Filled.DragHandle,
haptic?.performHapticFeedback(ReorderHapticFeedbackType.END) contentDescription = stringResource(R.string.stream_item_drag_handle)
onDragFinished() )
}, }
interactionSource = interactionSource,
)
}
} else {
Modifier
.aspectRatio(1f)
.fillMaxSize()
},
onClick = {}
) {
Icon(
imageVector = Icons.Filled.DragHandle,
contentDescription = stringResource(R.string.stream_item_drag_handle)
)
} }
} }
} }
@ -233,7 +264,8 @@ fun StreamItemPreview() {
haptic = null, haptic = null,
onDragFinished = {}, onDragFinished = {},
isDragging = false, isDragging = false,
isCurrentlyPlaying = true isCurrentlyPlaying = true,
onDelete = {println("has ben deleted")}
) )
} }
} }