From e19c967eaf6978c0519866e9ebffe561bfd8a008 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 18 Apr 2025 10:49:24 +0200 Subject: [PATCH 01/11] fix thumbnail prevew animation regression --- .../newplayer/ui/common/ThumbPreview.kt | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt index 9a2cde4..6dfbb78 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt @@ -98,10 +98,20 @@ internal fun ThumbPreview( else previewPosition - (previewBoxWidthPxls / 2 + boxPaddingPxls) + // This function is required to prevent the thumbnail to collapse and glitch during + // enter and exit animation. + fun getHeight(): Dp { + var internalHeight = (2 * BOX_PADDING).dp + previewHeight + if (uiState.currentSeekPreviewChapter != null) { + internalHeight += 16.dp + } + return internalHeight + } Box( modifier = modifier .fillMaxWidth() + .height(getHeight()) .onGloballyPositioned { rect -> sliderBoxWidth = rect.size.width } @@ -111,43 +121,47 @@ internal fun ThumbPreview( enter = fadeIn(animationSpec = tween(200)), exit = fadeOut(animationSpec = tween(400)), ) { - // this allows the current thumbnail to remain when animated visibility is being hidden - var lastAvailableImage by remember { - mutableStateOf(uiState.currentSeekPreviewThumbnail) - } - if (uiState.currentSeekPreviewThumbnail != null) { - lastAvailableImage = uiState.currentSeekPreviewThumbnail - } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .wrapContentSize() - .offset { IntOffset(edgeCorrectedPreviewPosition.toInt(), 0) }, - ) { - uiState.currentSeekPreviewChapter?.chapterTitle?.let { chapterTitle -> - Text( - text = chapterTitle, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodySmall, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - modifier = Modifier.width(previewHeight * aspectRatio) - ) + // Together with the getHeight() function this Box ensures that the thumbnail + // does not collapse and glitch during enter and exit animation. + Box(Modifier.fillMaxSize()) { + // this allows the current thumbnail to remain when animated visibility is being hidden + var lastAvailableImage by remember { + mutableStateOf(uiState.currentSeekPreviewThumbnail) + } + if (uiState.currentSeekPreviewThumbnail != null) { + lastAvailableImage = uiState.currentSeekPreviewThumbnail } - Card( + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .padding(BOX_PADDING.dp) - .height((2 * BOX_PADDING).dp + previewHeight) - .aspectRatio(aspectRatio), - elevation = CardDefaults.cardElevation(BOX_PADDING.dp) + .wrapContentSize() + .offset { IntOffset(edgeCorrectedPreviewPosition.toInt(), 0) }, ) { - lastAvailableImage?.let { - Image( - modifier = Modifier.fillMaxSize(), - bitmap = it, - contentDescription = stringResource(id = R.string.seek_thumb_preview) + uiState.currentSeekPreviewChapter?.chapterTitle?.let { chapterTitle -> + Text( + text = chapterTitle, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + modifier = Modifier.width(previewHeight * aspectRatio) ) } + Card( + modifier = Modifier + .padding(BOX_PADDING.dp) + .height((2 * BOX_PADDING).dp + previewHeight) + .aspectRatio(aspectRatio), + elevation = CardDefaults.cardElevation(BOX_PADDING.dp) + ) { + lastAvailableImage?.let { + Image( + modifier = Modifier.fillMaxSize(), + bitmap = it, + contentDescription = stringResource(id = R.string.seek_thumb_preview) + ) + } + } } } } From a4a7dc8510f2ba662fb3b6525fd12f97099abb5a Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 27 May 2025 12:00:22 +0200 Subject: [PATCH 02/11] move preview thumbnail geometry calculation into a seperate file --- .../newplayer/ui/audioplayer/AudioPlayerUI.kt | 2 +- .../{ => thumb_preview}/ThumbPreview.kt | 85 +++++++------------ .../ThumbPreviewGeopmetryCalculations.kt | 48 +++++++++++ .../ui/videoplayer/controller/BottomUI.kt | 2 +- 4 files changed, 82 insertions(+), 55 deletions(-) rename new-player/src/main/java/net/newpipe/newplayer/ui/common/{ => thumb_preview}/ThumbPreview.kt (73%) create mode 100644 new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerUI.kt index 66daac2..cf4f627 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/AudioPlayerUI.kt @@ -63,7 +63,7 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.util.UnstableApi import net.newpipe.newplayer.R import net.newpipe.newplayer.ui.common.NewPlayerSeeker -import net.newpipe.newplayer.ui.common.ThumbPreview +import net.newpipe.newplayer.ui.common.thumb_preview.ThumbPreview import net.newpipe.newplayer.ui.common.Thumbnail import net.newpipe.newplayer.ui.common.getInsets import net.newpipe.newplayer.ui.common.getLocale diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt similarity index 73% rename from new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt rename to new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index 6dfbb78..bacf8b3 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -1,4 +1,4 @@ -package net.newpipe.newplayer.ui.common +package net.newpipe.newplayer.ui.common.thumb_preview /* NewPlayer * @@ -31,7 +31,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -41,26 +40,23 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.media3.common.util.UnstableApi import net.newpipe.newplayer.R +import net.newpipe.newplayer.data.Chapter import net.newpipe.newplayer.ui.seeker.SeekerDefaults import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.uiModel.NewPlayerUIState -private const val BOX_PADDING = 4 +internal const val PREVIEW_BOX_PADDING = 4 +/** @hide */ @OptIn(UnstableApi::class) @Composable - -/** @hide */ internal fun ThumbPreview( modifier: Modifier = Modifier, uiState: NewPlayerUIState, @@ -70,52 +66,26 @@ internal fun ThumbPreview( previewHeight: Dp = 60.dp, ) { - val thumbSizePxls = with(LocalDensity.current) { thumbSize.toPx() } - val boxPaddingPxls = with(LocalDensity.current) { BOX_PADDING.dp.toPx() } - var sliderBoxWidth by remember { mutableIntStateOf(-1) } - val aspectRatio = if (uiState.currentSeekPreviewThumbnail != null) { - uiState.currentSeekPreviewThumbnail.width.toFloat() / - uiState.currentSeekPreviewThumbnail.height.toFloat() - } else { - 16f / 9f - } - - val previewBoxWidthPxls = with(LocalDensity.current) { (previewHeight * aspectRatio).toPx() } - - val previewPosition = additionalStartPaddingPxls + thumbSizePxls / 2 + - ((sliderBoxWidth - additionalEndPaddingPxls - additionalStartPaddingPxls - thumbSizePxls) - * uiState.seekerPosition) - - val edgeCorrectedPreviewPosition = - if (previewPosition < (previewBoxWidthPxls / 2 + boxPaddingPxls)) - 0 - else if ((sliderBoxWidth - (previewBoxWidthPxls / 2 + boxPaddingPxls)) < previewPosition) - sliderBoxWidth - previewBoxWidthPxls - 2 * boxPaddingPxls - else - previewPosition - (previewBoxWidthPxls / 2 + boxPaddingPxls) - - // This function is required to prevent the thumbnail to collapse and glitch during - // enter and exit animation. - fun getHeight(): Dp { - var internalHeight = (2 * BOX_PADDING).dp + previewHeight - if (uiState.currentSeekPreviewChapter != null) { - internalHeight += 16.dp - } - return internalHeight - } + val thumbnailGeometry = calculateThumbnailPreviewGeometry( + uiState = uiState, + thumbSize = thumbSize, + previewHeight = previewHeight, + sliderBoxWidth = sliderBoxWidth, + additionalStartPaddingPxls = additionalStartPaddingPxls, + additionalEndPaddingPxls = additionalEndPaddingPxls + ) Box( modifier = modifier .fillMaxWidth() - .height(getHeight()) + .height((2 * PREVIEW_BOX_PADDING).dp + previewHeight) .onGloballyPositioned { rect -> sliderBoxWidth = rect.size.width - } - ) { + }) { AnimatedVisibility( visible = uiState.seekPreviewVisible && uiState.currentSeekPreviewThumbnail != null, enter = fadeIn(animationSpec = tween(200)), @@ -123,7 +93,9 @@ internal fun ThumbPreview( ) { // Together with the getHeight() function this Box ensures that the thumbnail // does not collapse and glitch during enter and exit animation. - Box(Modifier.fillMaxSize()) { + Box(Modifier + .fillMaxSize() + .background(Color.Red)) { // this allows the current thumbnail to remain when animated visibility is being hidden var lastAvailableImage by remember { mutableStateOf(uiState.currentSeekPreviewThumbnail) @@ -135,8 +107,9 @@ internal fun ThumbPreview( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .wrapContentSize() - .offset { IntOffset(edgeCorrectedPreviewPosition.toInt(), 0) }, + .offset { IntOffset(thumbnailGeometry.edgeCorrectedPreviewPosition, 0) }, ) { + /* uiState.currentSeekPreviewChapter?.chapterTitle?.let { chapterTitle -> Text( text = chapterTitle, @@ -147,12 +120,15 @@ internal fun ThumbPreview( modifier = Modifier.width(previewHeight * aspectRatio) ) } + + */ + Card( modifier = Modifier - .padding(BOX_PADDING.dp) - .height((2 * BOX_PADDING).dp + previewHeight) - .aspectRatio(aspectRatio), - elevation = CardDefaults.cardElevation(BOX_PADDING.dp) + .padding(PREVIEW_BOX_PADDING.dp) + .height((2 * PREVIEW_BOX_PADDING).dp + previewHeight) + .aspectRatio(thumbnailGeometry.aspectRatio), + elevation = CardDefaults.cardElevation(PREVIEW_BOX_PADDING.dp) ) { lastAvailableImage?.let { Image( @@ -191,7 +167,7 @@ private fun ThumbPreviewPreview() { var thumbDown by remember { mutableStateOf(false) } val previewThumbnail = null -// BitmapFactory.decodeResource(LocalContext.current.resources, R.mipmap.thumbnail_preview) + BitmapFactory.decodeResource(LocalContext.current.resources, R.mipmap.thumbnail_preview) VideoPlayerTheme { @@ -204,8 +180,11 @@ private fun ThumbPreviewPreview() { uiState = NewPlayerUIState.DUMMY.copy( seekerPosition = sliderPosition, seekPreviewVisible = thumbDown, - currentSeekPreviewThumbnail = previewThumbnail?.asImageBitmap() - ), additionalStartPaddingPxls = startOffset, additionalEndPaddingPxls = endOffset, + currentSeekPreviewThumbnail = previewThumbnail.asImageBitmap(), + currentSeekPreviewChapter = Chapter(0, "What a wonderfull chapter", null) + ), + additionalStartPaddingPxls = startOffset, + additionalEndPaddingPxls = endOffset, thumbSize = 20.dp // see handle width ) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt new file mode 100644 index 0000000..a244480 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt @@ -0,0 +1,48 @@ +package net.newpipe.newplayer.ui.common.thumb_preview + +import androidx.annotation.OptIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.media3.common.util.UnstableApi +import net.newpipe.newplayer.uiModel.NewPlayerUIState + +data class ThumbnailGeometry( + val aspectRatio: Float, val edgeCorrectedPreviewPosition: Int +) + +@OptIn(UnstableApi::class) +@Composable +internal fun calculateThumbnailPreviewGeometry( + uiState: NewPlayerUIState, + thumbSize: Dp, + previewHeight: Dp, + sliderBoxWidth: Int, + additionalStartPaddingPxls: Int, + additionalEndPaddingPxls: Int, +): ThumbnailGeometry { + val thumbSizePxls = with(LocalDensity.current) { thumbSize.toPx() } + val boxPaddingPxls = with(LocalDensity.current) { PREVIEW_BOX_PADDING.dp.toPx() } + + val aspectRatio = if (uiState.currentSeekPreviewThumbnail != null) { + uiState.currentSeekPreviewThumbnail.width.toFloat() / uiState.currentSeekPreviewThumbnail.height.toFloat() + } else { + 16f / 9f + } + + val previewBoxWidthPxls = with(LocalDensity.current) { (previewHeight * aspectRatio).toPx() } + + val previewPosition = + additionalStartPaddingPxls + thumbSizePxls / 2 + ((sliderBoxWidth - additionalEndPaddingPxls - additionalStartPaddingPxls - thumbSizePxls) * uiState.seekerPosition) + + val edgeCorrectedPreviewPosition = + if (previewPosition < (previewBoxWidthPxls / 2 + boxPaddingPxls)) 0 + else if ((sliderBoxWidth - (previewBoxWidthPxls / 2 + boxPaddingPxls)) < previewPosition) sliderBoxWidth - previewBoxWidthPxls - 2 * boxPaddingPxls + else previewPosition - (previewBoxWidthPxls / 2 + boxPaddingPxls) + + return ThumbnailGeometry( + aspectRatio, edgeCorrectedPreviewPosition.toInt() + ) +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/BottomUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/BottomUI.kt index 9f8bc84..d7c0ede 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/BottomUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/BottomUI.kt @@ -56,7 +56,7 @@ import net.newpipe.newplayer.uiModel.NewPlayerUIState import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel import net.newpipe.newplayer.uiModel.NewPlayerViewModelDummy import net.newpipe.newplayer.ui.common.NewPlayerSeeker -import net.newpipe.newplayer.ui.common.ThumbPreview +import net.newpipe.newplayer.ui.common.thumb_preview.ThumbPreview import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.common.getEmbeddedUiConfig import net.newpipe.newplayer.ui.common.getLocale From ddc43dfe6365948971777949965df71fe35897ff Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 27 May 2025 12:11:40 +0200 Subject: [PATCH 03/11] make seek preview chapter to be seek preview text, so not only the chapter can be shown --- .../ui/common/thumb_preview/ThumbPreview.kt | 33 +++++++++++++++---- .../newplayer/uiModel/NewPlayerUIState.kt | 8 ++--- .../uiModel/NewPlayerViewModelImpl.kt | 27 +++++---------- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index bacf8b3..ee1bf53 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -28,12 +28,27 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Slider import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -93,9 +108,11 @@ internal fun ThumbPreview( ) { // Together with the getHeight() function this Box ensures that the thumbnail // does not collapse and glitch during enter and exit animation. - Box(Modifier - .fillMaxSize() - .background(Color.Red)) { + Box( + Modifier + .fillMaxSize() + .background(Color.Red) + ) { // this allows the current thumbnail to remain when animated visibility is being hidden var lastAvailableImage by remember { mutableStateOf(uiState.currentSeekPreviewThumbnail) @@ -181,7 +198,11 @@ private fun ThumbPreviewPreview() { seekerPosition = sliderPosition, seekPreviewVisible = thumbDown, currentSeekPreviewThumbnail = previewThumbnail.asImageBitmap(), - currentSeekPreviewChapter = Chapter(0, "What a wonderfull chapter", null) + currentSeekPreviewText = Chapter( + 0, + "What a wonderfull chapter", + null + ).chapterTitle ), additionalStartPaddingPxls = startOffset, additionalEndPaddingPxls = endOffset, diff --git a/new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerUIState.kt b/new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerUIState.kt index 35446f1..d2acc90 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerUIState.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerUIState.kt @@ -185,10 +185,10 @@ data class NewPlayerUIState( val currentSeekPreviewThumbnail: ImageBitmap?, /** - * The seeker preview chapter that should be visible. This updates if the user uses - * the seeker thumb to seek through a stream. If null no chapter is available. + * The seeker preview text that should be visible when the playback position is changed through + * the seek bar. If null no text will be rendered. */ - val currentSeekPreviewChapter: Chapter?, + val currentSeekPreviewText: String?, /** * Depicts weather the seeker preview thumbnail should be visible or not. @@ -230,7 +230,7 @@ data class NewPlayerUIState( currentlyPlayingTracks = emptyList(), enteringPip = false, currentSeekPreviewThumbnail = null, - currentSeekPreviewChapter = null, + currentSeekPreviewText = null, seekPreviewVisible = false, showPlaylistInAudioPlayer = false ) diff --git a/new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerViewModelImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerViewModelImpl.kt index da014af..ad65e3e 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerViewModelImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerViewModelImpl.kt @@ -499,8 +499,7 @@ class NewPlayerViewModelImpl @Inject constructor( newPlayer?.currentPosition = seekPositionInMs Log.i(TAG, "Seek to Ms: $seekPositionInMs") - updateSeekPreviewThumbnail(seekPositionInMs) - updateSeekPreviewChapter(seekPositionInMs) + updateSeekPreview(seekPositionInMs) mutableUiState.update { it.copy( seekerPosition = newValue, @@ -510,22 +509,7 @@ class NewPlayerViewModelImpl @Inject constructor( } } - private fun updateSeekPreviewChapter(seekPositionInMs: Long) { - viewModelScope.launch { - val chapters = mutableUiState.value.chapters - val chapter = chapters.lastOrNull { - it.chapterStartInMs < seekPositionInMs - } - mutableUiState.update { - it.copy( - currentSeekPreviewChapter = chapter, - seekPreviewVisible = true - ) - } - } - } - - private fun updateSeekPreviewThumbnail(seekPositionInMs: Long) { + private fun updateSeekPreview(seekPositionInMs: Long) { updatePreviewThumbnailJob?.cancel() updatePreviewThumbnailJob = viewModelScope.launch { @@ -533,12 +517,19 @@ class NewPlayerViewModelImpl @Inject constructor( val item = newPlayer?.currentlyPlaying?.value?.let { newPlayer?.getItemFromMediaItem(it) } + + val chapters = mutableUiState.value.chapters + val chapter = chapters.lastOrNull { + it.chapterStartInMs < seekPositionInMs + } + item?.let { val bitmap = newPlayer?.repository?.getPreviewThumbnail(item, seekPositionInMs) mutableUiState.update { it.copy( + currentSeekPreviewText = chapter?.chapterTitle, currentSeekPreviewThumbnail = bitmap?.asImageBitmap(), seekPreviewVisible = true ) From 4cc0204982fa314f1dbd68e4787a7528c5ecdb3a Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 27 May 2025 12:20:39 +0200 Subject: [PATCH 04/11] add hidden messure helper --- .../net/newpipe/newplayer/ui/common/utils.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/utils.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/utils.kt index f01886f..553c63f 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/utils.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/utils.kt @@ -34,6 +34,7 @@ import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.waterfall @@ -42,10 +43,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.DpSize import androidx.core.os.ConfigurationCompat import androidx.core.view.WindowCompat import androidx.media3.common.MediaItem @@ -266,4 +271,21 @@ internal fun relaunchCurrentActivity(activity: Activity) { */ activity.startActivity(activity.intent) +} + +@Composable +fun HiddenMeasure( + content: @Composable () -> Unit, + onMeasured: (Placeable) -> Unit +) { + Layout( + modifier = Modifier.size(DpSize.Zero), + content = content + ) { measurable, _ -> + val placeable = measurable.first().measure(Constraints()) + onMeasured(placeable) + layout(0, 0) { + // Draw nothing + } + } } \ No newline at end of file From c00c5e6ee37937b898a34dca8cbf49909abca2b5 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 27 May 2025 12:43:13 +0200 Subject: [PATCH 05/11] create seperatly sliding text scaffold --- .../newplayer/ui/audioplayer/ProgressUi.kt | 2 +- .../ui/common/thumb_preview/ThumbPreview.kt | 81 ++++++++++++++----- .../ThumbPreviewGeopmetryCalculations.kt | 5 +- .../net/newpipe/newplayer/ui/common/utils.kt | 33 +++----- 4 files changed, 80 insertions(+), 41 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/ProgressUi.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/ProgressUi.kt index d05494f..2aea37f 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/ProgressUi.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/ProgressUi.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.media3.common.util.UnstableApi import net.newpipe.newplayer.ui.common.NewPlayerSeeker -import net.newpipe.newplayer.ui.common.ThumbPreview +import net.newpipe.newplayer.ui.common.thumb_preview.ThumbPreview import net.newpipe.newplayer.ui.common.getLocale import net.newpipe.newplayer.ui.common.getTimeStringFromMs import net.newpipe.newplayer.ui.seeker.SeekerDefaults diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index ee1bf53..dc95e42 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -40,6 +40,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -56,6 +57,8 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset @@ -67,6 +70,7 @@ import net.newpipe.newplayer.ui.seeker.SeekerDefaults import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.uiModel.NewPlayerUIState +/** @hide */ internal const val PREVIEW_BOX_PADDING = 4 /** @hide */ @@ -81,6 +85,62 @@ internal fun ThumbPreview( previewHeight: Dp = 60.dp, ) { + Column(modifier = modifier) { + ThumbTextPreview(modifier = Modifier, uiState) + + ThumbImagePreview( + modifier = Modifier, + uiState, + thumbSize, + additionalStartPaddingPxls, + additionalEndPaddingPxls, + previewHeight + ) + } +} + +@OptIn(UnstableApi::class) +@Composable +private fun ThumbTextPreview( + modifier: Modifier = Modifier, + uiState: NewPlayerUIState, +) { + + val textHeight = 30.dp + + var sliderBoxWidth by remember { + mutableIntStateOf(-1) + } + + Box( + modifier = modifier + .fillMaxWidth() + .height((2 * PREVIEW_BOX_PADDING).dp + textHeight) + .onGloballyPositioned { rect -> + sliderBoxWidth = rect.size.width + }) { + + Text( + text = uiState.currentSeekPreviewText ?: "", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + } +} + +@OptIn(UnstableApi::class) +@Composable +private fun ThumbImagePreview( + modifier: Modifier = Modifier, + uiState: NewPlayerUIState, + thumbSize: Dp = SeekerDefaults.ThumbRadius * 2, + additionalStartPaddingPxls: Int = 0, + additionalEndPaddingPxls: Int = 0, + previewHeight: Dp = 60.dp, +) { + var sliderBoxWidth by remember { mutableIntStateOf(-1) } @@ -120,26 +180,11 @@ internal fun ThumbPreview( if (uiState.currentSeekPreviewThumbnail != null) { lastAvailableImage = uiState.currentSeekPreviewThumbnail } - Column( - horizontalAlignment = Alignment.CenterHorizontally, + Box( modifier = Modifier .wrapContentSize() .offset { IntOffset(thumbnailGeometry.edgeCorrectedPreviewPosition, 0) }, ) { - /* - uiState.currentSeekPreviewChapter?.chapterTitle?.let { chapterTitle -> - Text( - text = chapterTitle, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodySmall, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - modifier = Modifier.width(previewHeight * aspectRatio) - ) - } - - */ - Card( modifier = Modifier .padding(PREVIEW_BOX_PADDING.dp) @@ -172,7 +217,7 @@ internal fun ThumbPreview( } } - @OptIn(UnstableApi::class) +@OptIn(UnstableApi::class) @Preview(device = "spec:width=1080px,height=600px,dpi=440") @Composable private fun ThumbPreviewPreview() { @@ -183,7 +228,7 @@ private fun ThumbPreviewPreview() { var thumbDown by remember { mutableStateOf(false) } - val previewThumbnail = null + val previewThumbnail = BitmapFactory.decodeResource(LocalContext.current.resources, R.mipmap.thumbnail_preview) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt index a244480..de344fc 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt @@ -5,14 +5,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.media3.common.util.UnstableApi import net.newpipe.newplayer.uiModel.NewPlayerUIState -data class ThumbnailGeometry( +/** @hide */ +internal data class ThumbnailGeometry( val aspectRatio: Float, val edgeCorrectedPreviewPosition: Int ) +/** @hide */ @OptIn(UnstableApi::class) @Composable internal fun calculateThumbnailPreviewGeometry( diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/utils.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/utils.kt index 553c63f..d0be97d 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/utils.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/utils.kt @@ -56,8 +56,8 @@ import androidx.core.view.WindowCompat import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import coil.compose.AsyncImage -import net.newpipe.newplayer.data.NewPlayerException import net.newpipe.newplayer.R +import net.newpipe.newplayer.data.NewPlayerException import net.newpipe.newplayer.uiModel.EmbeddedUiConfig import java.util.Locale @@ -67,8 +67,7 @@ import java.util.Locale */ /** @hide */ @Composable -internal fun activity(): Activity - = LocalContext.current.findActivity()!! +internal fun activity(): Activity = LocalContext.current.findActivity()!! /** Call block with the [Activity] from current context, if there is an activity. * @@ -77,7 +76,7 @@ internal fun activity(): Activity */ /** @hide */ @Composable -internal fun activity(default: T, block: @Composable Activity.() -> T): T = +internal fun activity(default: T, block: @Composable Activity.() -> T): T = when (val a = LocalContext.current.findActivity()) { null -> default else -> block(a) @@ -85,8 +84,7 @@ internal fun activity(default: T, block: @Composable Activity.() -> T): T = /** @hide */ @Composable -internal fun window(): Window - = activity().window +internal fun window(): Window = activity().window /** @hide */ internal fun Context.findActivity(): Activity? = when (this) { @@ -113,8 +111,8 @@ internal fun Activity.getDefaultBrightness(): Float { return if (layout.screenBrightness < 0) -1f else layout.screenBrightness } -@SuppressLint("NewApi") /** @hide */ +@SuppressLint("NewApi") internal fun setScreenBrightness(value: Float, activity: Activity) { val window = activity.window val layout = window.attributes as WindowManager.LayoutParams @@ -123,11 +121,9 @@ internal fun setScreenBrightness(value: Float, activity: Activity) { } - - +/** @hide */ @Composable @ReadOnlyComposable -/** @hide */ internal fun getLocale(): Locale? { val configuration = LocalConfiguration.current return ConfigurationCompat.getLocales(configuration).get(0) @@ -136,8 +132,7 @@ internal fun getLocale(): Locale? { @Composable /** @return A collection of current activity/window configurations */ /** @hide */ -internal fun getEmbeddedUiConfig() - = activity(EmbeddedUiConfig.DUMMY) { getEmbeddedUiConfig() } +internal fun getEmbeddedUiConfig() = activity(EmbeddedUiConfig.DUMMY) { getEmbeddedUiConfig() } @Composable @ReadOnlyComposable @@ -159,8 +154,8 @@ internal fun Activity.getEmbeddedUiConfig(): EmbeddedUiConfig { ) } -@Composable /** @hide */ +@Composable internal fun getInsets() = WindowInsets.systemBars.union(WindowInsets.displayCutout).union(WindowInsets.waterfall) @@ -203,9 +198,8 @@ internal fun getTimeStringFromMs( return time_string } -@Composable - /** @hide */ +@Composable internal fun Thumbnail( modifier: Modifier = Modifier, thumbnail: Uri?, @@ -236,18 +230,16 @@ internal fun Thumbnail( } } +/** @hide */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @Composable - -/** @hide */ internal fun isInPowerSaveMode() = (LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager) .isPowerSaveMode -@OptIn(UnstableApi::class) - /** @hide */ +@OptIn(UnstableApi::class) internal fun getPlaylistDurationInMS(playlist: List): Long { var duration = 0L for (item in playlist) { @@ -273,8 +265,9 @@ internal fun relaunchCurrentActivity(activity: Activity) { } +/** @hide */ @Composable -fun HiddenMeasure( +internal fun HiddenMeasure( content: @Composable () -> Unit, onMeasured: (Placeable) -> Unit ) { From ca53234fccdb42527f7869608ab9bd4628e29560 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 30 May 2025 16:20:27 +0200 Subject: [PATCH 06/11] make text preview appear and hide animated --- .../ui/common/thumb_preview/ThumbPreview.kt | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index dc95e42..645d21c 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -73,6 +73,9 @@ import net.newpipe.newplayer.uiModel.NewPlayerUIState /** @hide */ internal const val PREVIEW_BOX_PADDING = 4 +private val PREVIEW_FADE_IN = fadeIn(tween(200)) +private val PREVIEW_FADE_OUT = fadeOut(tween(400)) + /** @hide */ @OptIn(UnstableApi::class) @Composable @@ -120,13 +123,23 @@ private fun ThumbTextPreview( sliderBoxWidth = rect.size.width }) { - Text( - text = uiState.currentSeekPreviewText ?: "", - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - ) + AnimatedVisibility( + visible = uiState.seekPreviewVisible && (uiState.currentSeekPreviewText ?: "") != "", + enter = PREVIEW_FADE_IN, + exit = PREVIEW_FADE_OUT + ) { + Box(modifier = Modifier + .fillMaxSize() + .background(Color.Yellow)) { + Text( + text = uiState.currentSeekPreviewText ?: "", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + } + } } } @@ -163,8 +176,8 @@ private fun ThumbImagePreview( }) { AnimatedVisibility( visible = uiState.seekPreviewVisible && uiState.currentSeekPreviewThumbnail != null, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(400)), + enter = PREVIEW_FADE_IN, + exit = PREVIEW_FADE_OUT ) { // Together with the getHeight() function this Box ensures that the thumbnail // does not collapse and glitch during enter and exit animation. From 722bc55216c951f79209733b67488b0c14116094 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 2 Jun 2025 15:27:53 +0200 Subject: [PATCH 07/11] use SubcomposeLayout to do thumb relative text positioning --- .../ui/common/thumb_preview/ThumbPreview.kt | 40 ++++++++--- .../ThumbPreviewGeopmetryCalculations.kt | 70 +++++++++++++++++++ 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index 645d21c..1bfa9f3 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -76,6 +76,7 @@ internal const val PREVIEW_BOX_PADDING = 4 private val PREVIEW_FADE_IN = fadeIn(tween(200)) private val PREVIEW_FADE_OUT = fadeOut(tween(400)) + /** @hide */ @OptIn(UnstableApi::class) @Composable @@ -89,7 +90,10 @@ internal fun ThumbPreview( ) { Column(modifier = modifier) { - ThumbTextPreview(modifier = Modifier, uiState) + ThumbTextPreview( + modifier = Modifier, uiState = uiState, thumbSize = thumbSize, + startOffset = additionalStartPaddingPxls, endOffset = additionalEndPaddingPxls + ) ThumbImagePreview( modifier = Modifier, @@ -107,6 +111,9 @@ internal fun ThumbPreview( private fun ThumbTextPreview( modifier: Modifier = Modifier, uiState: NewPlayerUIState, + thumbSize: Dp, + startOffset: Int, + endOffset: Int ) { val textHeight = 30.dp @@ -128,16 +135,27 @@ private fun ThumbTextPreview( enter = PREVIEW_FADE_IN, exit = PREVIEW_FADE_OUT ) { - Box(modifier = Modifier - .fillMaxSize() - .background(Color.Yellow)) { - Text( - text = uiState.currentSeekPreviewText ?: "", - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - ) + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Yellow) + ) { + + PlaceCentralToThumb( + Modifier, + uiState, + thumbSize, + startOffset = startOffset, + endOffset = endOffset + ) { + Text( + text = uiState.currentSeekPreviewText ?: "", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + } } } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt index de344fc..1c3785f 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt @@ -2,7 +2,11 @@ package net.newpipe.newplayer.ui.common.thumb_preview import androidx.annotation.OptIn import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.media3.common.util.UnstableApi @@ -13,6 +17,72 @@ internal data class ThumbnailGeometry( val aspectRatio: Float, val edgeCorrectedPreviewPosition: Int ) +/**hide*/ +internal data class ThumbTextPreviewGeometry( + val text: String, + val size: Placeable +) + +/** hide */ +@OptIn(UnstableApi::class) +@Composable +internal fun PlaceCentralToThumb( + modifier: Modifier, + uiState: NewPlayerUIState, + thumbSize: Dp, + startOffset: Int, + endOffset: Int, + content: @Composable () -> Unit +) { + val thumbSizePxls = with(LocalDensity.current) { thumbSize.toPx() }.toInt() + + SubcomposeLayout { constraints -> + val placeables = subcompose(null, content).map { + it.measure(Constraints()) + } + + + val xPositions = placeables.map { + calculateThumbRelativeXCoordinates( + uiState = uiState, + thumbSize = thumbSizePxls, + placeable = it, + startOffset = startOffset, + endOffset = endOffset, + constraints = constraints + ) + } + + layout(constraints.maxWidth, constraints.maxHeight) { + placeables.zip(xPositions).map { (placeable, xPosition) -> + placeable.place(xPosition, 0) + } + } + } +} + +/** hide */ +@OptIn(UnstableApi::class) +internal fun calculateThumbRelativeXCoordinates( + uiState: NewPlayerUIState, + thumbSize: Int, + placeable: Placeable, + constraints: Constraints, + startOffset: Int, + endOffset: Int, +): Int { + + val thumbCenterLocationX = + startOffset + thumbSize / 2 + ((constraints.maxWidth - thumbSize - startOffset - endOffset) * uiState.seekerPosition) + + val edgeCorrectedPreviewPosition = + if (thumbCenterLocationX < (placeable.width / 2)) 0 + else if ((constraints.maxWidth - (placeable.width / 2)) < thumbCenterLocationX) constraints.maxWidth - placeable.width + else thumbCenterLocationX - (placeable.width / 2) + + return edgeCorrectedPreviewPosition.toInt() +} + /** @hide */ @OptIn(UnstableApi::class) @Composable From f3bedf6ad3bc10ad841452c07984e7bdfc1f4e65 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 2 Jun 2025 15:57:14 +0200 Subject: [PATCH 08/11] also plase preview thumbnail with SubcomposeLayout --- .../ui/common/thumb_preview/ThumbPreview.kt | 51 ++++++++++--------- .../ThumbPreviewGeopmetryCalculations.kt | 37 +------------- 2 files changed, 27 insertions(+), 61 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index 1bfa9f3..8263edb 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -38,6 +38,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme @@ -141,7 +142,7 @@ private fun ThumbTextPreview( .background(Color.Yellow) ) { - PlaceCentralToThumb( + PlaceRelativeToThumbSliderLayout( Modifier, uiState, thumbSize, @@ -176,15 +177,6 @@ private fun ThumbImagePreview( mutableIntStateOf(-1) } - val thumbnailGeometry = calculateThumbnailPreviewGeometry( - uiState = uiState, - thumbSize = thumbSize, - previewHeight = previewHeight, - sliderBoxWidth = sliderBoxWidth, - additionalStartPaddingPxls = additionalStartPaddingPxls, - additionalEndPaddingPxls = additionalEndPaddingPxls - ) - Box( modifier = modifier .fillMaxWidth() @@ -211,27 +203,36 @@ private fun ThumbImagePreview( if (uiState.currentSeekPreviewThumbnail != null) { lastAvailableImage = uiState.currentSeekPreviewThumbnail } - Box( - modifier = Modifier - .wrapContentSize() - .offset { IntOffset(thumbnailGeometry.edgeCorrectedPreviewPosition, 0) }, + + PlaceRelativeToThumbSliderLayout( + Modifier, + uiState = uiState, + thumbSize = thumbSize, + startOffset = additionalStartPaddingPxls, + endOffset = additionalEndPaddingPxls ) { - Card( + Box( modifier = Modifier - .padding(PREVIEW_BOX_PADDING.dp) - .height((2 * PREVIEW_BOX_PADDING).dp + previewHeight) - .aspectRatio(thumbnailGeometry.aspectRatio), - elevation = CardDefaults.cardElevation(PREVIEW_BOX_PADDING.dp) + .wrapContentSize() ) { - lastAvailableImage?.let { - Image( - modifier = Modifier.fillMaxSize(), - bitmap = it, - contentDescription = stringResource(id = R.string.seek_thumb_preview) - ) + Card( + modifier = Modifier + .padding(PREVIEW_BOX_PADDING.dp) + .height(previewHeight) + .wrapContentWidth(), + elevation = CardDefaults.cardElevation(PREVIEW_BOX_PADDING.dp) + ) { + lastAvailableImage?.let { + Image( + modifier = Modifier.fillMaxSize(), + bitmap = it, + contentDescription = stringResource(id = R.string.seek_thumb_preview) + ) + } } } } + } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt index 1c3785f..ae25a36 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt @@ -26,7 +26,7 @@ internal data class ThumbTextPreviewGeometry( /** hide */ @OptIn(UnstableApi::class) @Composable -internal fun PlaceCentralToThumb( +internal fun PlaceRelativeToThumbSliderLayout( modifier: Modifier, uiState: NewPlayerUIState, thumbSize: Dp, @@ -81,39 +81,4 @@ internal fun calculateThumbRelativeXCoordinates( else thumbCenterLocationX - (placeable.width / 2) return edgeCorrectedPreviewPosition.toInt() -} - -/** @hide */ -@OptIn(UnstableApi::class) -@Composable -internal fun calculateThumbnailPreviewGeometry( - uiState: NewPlayerUIState, - thumbSize: Dp, - previewHeight: Dp, - sliderBoxWidth: Int, - additionalStartPaddingPxls: Int, - additionalEndPaddingPxls: Int, -): ThumbnailGeometry { - val thumbSizePxls = with(LocalDensity.current) { thumbSize.toPx() } - val boxPaddingPxls = with(LocalDensity.current) { PREVIEW_BOX_PADDING.dp.toPx() } - - val aspectRatio = if (uiState.currentSeekPreviewThumbnail != null) { - uiState.currentSeekPreviewThumbnail.width.toFloat() / uiState.currentSeekPreviewThumbnail.height.toFloat() - } else { - 16f / 9f - } - - val previewBoxWidthPxls = with(LocalDensity.current) { (previewHeight * aspectRatio).toPx() } - - val previewPosition = - additionalStartPaddingPxls + thumbSizePxls / 2 + ((sliderBoxWidth - additionalEndPaddingPxls - additionalStartPaddingPxls - thumbSizePxls) * uiState.seekerPosition) - - val edgeCorrectedPreviewPosition = - if (previewPosition < (previewBoxWidthPxls / 2 + boxPaddingPxls)) 0 - else if ((sliderBoxWidth - (previewBoxWidthPxls / 2 + boxPaddingPxls)) < previewPosition) sliderBoxWidth - previewBoxWidthPxls - 2 * boxPaddingPxls - else previewPosition - (previewBoxWidthPxls / 2 + boxPaddingPxls) - - return ThumbnailGeometry( - aspectRatio, edgeCorrectedPreviewPosition.toInt() - ) } \ No newline at end of file From 41ab69f01ba964840e629101c96fe62dbf9e2b4b Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 2 Jun 2025 16:09:37 +0200 Subject: [PATCH 09/11] cleanup slider preview --- .../ui/common/thumb_preview/ThumbPreview.kt | 156 +++++++----------- 1 file changed, 59 insertions(+), 97 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index 8263edb..2c7d07f 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -31,11 +31,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth @@ -62,7 +60,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.media3.common.util.UnstableApi import net.newpipe.newplayer.R @@ -92,8 +89,11 @@ internal fun ThumbPreview( Column(modifier = modifier) { ThumbTextPreview( - modifier = Modifier, uiState = uiState, thumbSize = thumbSize, - startOffset = additionalStartPaddingPxls, endOffset = additionalEndPaddingPxls + modifier = Modifier, + uiState = uiState, + thumbSize = thumbSize, + startOffset = additionalStartPaddingPxls, + endOffset = additionalEndPaddingPxls ) ThumbImagePreview( @@ -119,49 +119,30 @@ private fun ThumbTextPreview( val textHeight = 30.dp - var sliderBoxWidth by remember { - mutableIntStateOf(-1) - } - - Box( + AnimatedVisibility( modifier = modifier .fillMaxWidth() - .height((2 * PREVIEW_BOX_PADDING).dp + textHeight) - .onGloballyPositioned { rect -> - sliderBoxWidth = rect.size.width - }) { + .height((2 * PREVIEW_BOX_PADDING).dp + textHeight), + visible = uiState.seekPreviewVisible && (uiState.currentSeekPreviewText ?: "") != "", + enter = PREVIEW_FADE_IN, + exit = PREVIEW_FADE_OUT + ) { - AnimatedVisibility( - visible = uiState.seekPreviewVisible && (uiState.currentSeekPreviewText ?: "") != "", - enter = PREVIEW_FADE_IN, - exit = PREVIEW_FADE_OUT + PlaceRelativeToThumbSliderLayout( + Modifier, uiState, thumbSize, startOffset = startOffset, endOffset = endOffset ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Yellow) - ) { - - PlaceRelativeToThumbSliderLayout( - Modifier, - uiState, - thumbSize, - startOffset = startOffset, - endOffset = endOffset - ) { - Text( - text = uiState.currentSeekPreviewText ?: "", - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - ) - } - } + Text( + text = uiState.currentSeekPreviewText ?: "", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) } } } + @OptIn(UnstableApi::class) @Composable private fun ThumbImagePreview( @@ -173,70 +154,53 @@ private fun ThumbImagePreview( previewHeight: Dp = 60.dp, ) { - var sliderBoxWidth by remember { - mutableIntStateOf(-1) - } - - Box( - modifier = modifier + AnimatedVisibility( + modifier = Modifier .fillMaxWidth() - .height((2 * PREVIEW_BOX_PADDING).dp + previewHeight) - .onGloballyPositioned { rect -> - sliderBoxWidth = rect.size.width - }) { - AnimatedVisibility( - visible = uiState.seekPreviewVisible && uiState.currentSeekPreviewThumbnail != null, - enter = PREVIEW_FADE_IN, - exit = PREVIEW_FADE_OUT + .height(previewHeight + PREVIEW_BOX_PADDING.dp * 2), + visible = uiState.seekPreviewVisible && uiState.currentSeekPreviewThumbnail != null, + enter = PREVIEW_FADE_IN, + exit = PREVIEW_FADE_OUT + ) { + // Together with the getHeight() function this Box ensures that the thumbnail + // does not collapse and glitch during enter and exit animation. + + // this allows the current thumbnail to remain when animated visibility is being hidden + var lastAvailableImage by remember { + mutableStateOf(uiState.currentSeekPreviewThumbnail) + } + if (uiState.currentSeekPreviewThumbnail != null) { + lastAvailableImage = uiState.currentSeekPreviewThumbnail + } + + PlaceRelativeToThumbSliderLayout( + Modifier.wrapContentSize(), + uiState = uiState, + thumbSize = thumbSize, + startOffset = additionalStartPaddingPxls, + endOffset = additionalEndPaddingPxls ) { - // Together with the getHeight() function this Box ensures that the thumbnail - // does not collapse and glitch during enter and exit animation. Box( - Modifier - .fillMaxSize() - .background(Color.Red) + modifier = Modifier.wrapContentSize() ) { - // this allows the current thumbnail to remain when animated visibility is being hidden - var lastAvailableImage by remember { - mutableStateOf(uiState.currentSeekPreviewThumbnail) - } - if (uiState.currentSeekPreviewThumbnail != null) { - lastAvailableImage = uiState.currentSeekPreviewThumbnail - } - - PlaceRelativeToThumbSliderLayout( - Modifier, - uiState = uiState, - thumbSize = thumbSize, - startOffset = additionalStartPaddingPxls, - endOffset = additionalEndPaddingPxls + Card( + modifier = Modifier + .padding(PREVIEW_BOX_PADDING.dp) + .height(previewHeight) + .wrapContentWidth(), + elevation = CardDefaults.cardElevation(PREVIEW_BOX_PADDING.dp) ) { - Box( - modifier = Modifier - .wrapContentSize() - ) { - Card( - modifier = Modifier - .padding(PREVIEW_BOX_PADDING.dp) - .height(previewHeight) - .wrapContentWidth(), - elevation = CardDefaults.cardElevation(PREVIEW_BOX_PADDING.dp) - ) { - lastAvailableImage?.let { - Image( - modifier = Modifier.fillMaxSize(), - bitmap = it, - contentDescription = stringResource(id = R.string.seek_thumb_preview) - ) - } - } + lastAvailableImage?.let { + Image( + modifier = Modifier.fillMaxSize(), + bitmap = it, + contentDescription = stringResource(id = R.string.seek_thumb_preview) + ) } } - } } - /* This is a little helper block that helps place the thumbnail correctly relative to the thumb of the seeker. This is only there for debug reasons. Surface( @@ -276,9 +240,7 @@ private fun ThumbPreviewPreview() { seekPreviewVisible = thumbDown, currentSeekPreviewThumbnail = previewThumbnail.asImageBitmap(), currentSeekPreviewText = Chapter( - 0, - "What a wonderfull chapter", - null + 0, "What a wonderfull chapter", null ).chapterTitle ), additionalStartPaddingPxls = startOffset, From cde4b81e911f144721a1a154734c3b030512d7af Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 2 Jun 2025 16:43:45 +0200 Subject: [PATCH 10/11] fix thumbnail preview aspect ratio issue --- .../newplayer/ui/common/thumb_preview/ThumbPreview.kt | 7 ++++--- .../thumb_preview/ThumbPreviewGeopmetryCalculations.kt | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index 2c7d07f..29a4680 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -173,6 +174,7 @@ private fun ThumbImagePreview( lastAvailableImage = uiState.currentSeekPreviewThumbnail } + lastAvailableImage?.let { lastAvailableImage -> PlaceRelativeToThumbSliderLayout( Modifier.wrapContentSize(), uiState = uiState, @@ -187,13 +189,12 @@ private fun ThumbImagePreview( modifier = Modifier .padding(PREVIEW_BOX_PADDING.dp) .height(previewHeight) - .wrapContentWidth(), + .aspectRatio(lastAvailableImage.width.toFloat() / lastAvailableImage.height.toFloat()), elevation = CardDefaults.cardElevation(PREVIEW_BOX_PADDING.dp) ) { - lastAvailableImage?.let { Image( modifier = Modifier.fillMaxSize(), - bitmap = it, + bitmap = lastAvailableImage, contentDescription = stringResource(id = R.string.seek_thumb_preview) ) } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt index ae25a36..fb75d2d 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt @@ -36,7 +36,7 @@ internal fun PlaceRelativeToThumbSliderLayout( ) { val thumbSizePxls = with(LocalDensity.current) { thumbSize.toPx() }.toInt() - SubcomposeLayout { constraints -> + SubcomposeLayout(modifier) { constraints -> val placeables = subcompose(null, content).map { it.measure(Constraints()) } From ce92bac464befc67acd479dc9837e7de4626b11b Mon Sep 17 00:00:00 2001 From: lm41 Date: Sat, 16 Aug 2025 10:38:08 +0200 Subject: [PATCH 11/11] Fix text width --- .../ui/common/thumb_preview/ThumbPreview.kt | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt index 29a4680..6f3e8c7 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -36,8 +36,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme @@ -71,6 +71,7 @@ import net.newpipe.newplayer.uiModel.NewPlayerUIState /** @hide */ internal const val PREVIEW_BOX_PADDING = 4 +internal const val PREVIEW_TEXT_TO_HEIGHT_RATIO = 18 / 9 private val PREVIEW_FADE_IN = fadeIn(tween(200)) private val PREVIEW_FADE_OUT = fadeOut(tween(400)) @@ -94,7 +95,8 @@ internal fun ThumbPreview( uiState = uiState, thumbSize = thumbSize, startOffset = additionalStartPaddingPxls, - endOffset = additionalEndPaddingPxls + endOffset = additionalEndPaddingPxls, + previewHeight = previewHeight, ) ThumbImagePreview( @@ -103,7 +105,7 @@ internal fun ThumbPreview( thumbSize, additionalStartPaddingPxls, additionalEndPaddingPxls, - previewHeight + previewHeight, ) } } @@ -115,7 +117,8 @@ private fun ThumbTextPreview( uiState: NewPlayerUIState, thumbSize: Dp, startOffset: Int, - endOffset: Int + endOffset: Int, + previewHeight: Dp, ) { val textHeight = 30.dp @@ -133,6 +136,7 @@ private fun ThumbTextPreview( Modifier, uiState, thumbSize, startOffset = startOffset, endOffset = endOffset ) { Text( + modifier = Modifier.width(previewHeight * PREVIEW_TEXT_TO_HEIGHT_RATIO), text = uiState.currentSeekPreviewText ?: "", textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium, @@ -175,23 +179,23 @@ private fun ThumbImagePreview( } lastAvailableImage?.let { lastAvailableImage -> - PlaceRelativeToThumbSliderLayout( - Modifier.wrapContentSize(), - uiState = uiState, - thumbSize = thumbSize, - startOffset = additionalStartPaddingPxls, - endOffset = additionalEndPaddingPxls - ) { - Box( - modifier = Modifier.wrapContentSize() + PlaceRelativeToThumbSliderLayout( + Modifier.wrapContentSize(), + uiState = uiState, + thumbSize = thumbSize, + startOffset = additionalStartPaddingPxls, + endOffset = additionalEndPaddingPxls ) { - Card( - modifier = Modifier - .padding(PREVIEW_BOX_PADDING.dp) - .height(previewHeight) - .aspectRatio(lastAvailableImage.width.toFloat() / lastAvailableImage.height.toFloat()), - elevation = CardDefaults.cardElevation(PREVIEW_BOX_PADDING.dp) + Box( + modifier = Modifier.wrapContentSize() ) { + Card( + modifier = Modifier + .padding(PREVIEW_BOX_PADDING.dp) + .height(previewHeight) + .aspectRatio(lastAvailableImage.width.toFloat() / lastAvailableImage.height.toFloat()), + elevation = CardDefaults.cardElevation(PREVIEW_BOX_PADDING.dp) + ) { Image( modifier = Modifier.fillMaxSize(), bitmap = lastAvailableImage,