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/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/ThumbPreview.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt deleted file mode 100644 index 9a2cde4..0000000 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/common/ThumbPreview.kt +++ /dev/null @@ -1,214 +0,0 @@ -package net.newpipe.newplayer.ui.common - -/* NewPlayer - * - * @author Christian Schabesberger - * - * Copyright (C) NewPipe e.V. 2024 - * - * NewPlayer is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPlayer is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPlayer. If not, see . - */ - -import android.graphics.BitmapFactory -import androidx.annotation.OptIn -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -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.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.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -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.ui.seeker.SeekerDefaults -import net.newpipe.newplayer.ui.theme.VideoPlayerTheme -import net.newpipe.newplayer.uiModel.NewPlayerUIState - -private const val BOX_PADDING = 4 - -@OptIn(UnstableApi::class) -@Composable - -/** @hide */ -internal fun ThumbPreview( - modifier: Modifier = Modifier, - uiState: NewPlayerUIState, - thumbSize: Dp = SeekerDefaults.ThumbRadius * 2, - additionalStartPaddingPxls: Int = 0, - additionalEndPaddingPxls: Int = 0, - 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) - - - Box( - modifier = modifier - .fillMaxWidth() - .onGloballyPositioned { rect -> - sliderBoxWidth = rect.size.width - } - ) { - AnimatedVisibility( - visible = uiState.seekPreviewVisible && uiState.currentSeekPreviewThumbnail != null, - 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) - ) - } - 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) - ) - } - } - } - } - - - /* 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( - modifier = Modifier - .size(10.dp, 10.dp) - .offset { IntOffset(previewPosition.toInt(), 200) }, color = Color.Blue - ) { - } - */ - } -} - - @OptIn(UnstableApi::class) -@Preview(device = "spec:width=1080px,height=600px,dpi=440") -@Composable -private fun ThumbPreviewPreview() { - var sliderPosition by remember { mutableFloatStateOf(0f) } - - var startOffset by remember { mutableIntStateOf(0) } - var endOffset by remember { mutableIntStateOf(0) } - - var thumbDown by remember { mutableStateOf(false) } - - val previewThumbnail = null -// BitmapFactory.decodeResource(LocalContext.current.resources, R.mipmap.thumbnail_preview) - - - VideoPlayerTheme { - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.Green) - ) { - ThumbPreview( - uiState = NewPlayerUIState.DUMMY.copy( - seekerPosition = sliderPosition, - seekPreviewVisible = thumbDown, - currentSeekPreviewThumbnail = previewThumbnail?.asImageBitmap() - ), additionalStartPaddingPxls = startOffset, additionalEndPaddingPxls = endOffset, - thumbSize = 20.dp // see handle width - ) - - Row( - modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically - ) { - Text(text = "Left", modifier = Modifier.onGloballyPositioned { - startOffset = it.size.width - }) - Slider(modifier = Modifier.weight(1f), value = sliderPosition, onValueChange = { - thumbDown = true - sliderPosition = it - }, onValueChangeFinished = { thumbDown = false }) - Text(text = "R", modifier = Modifier.onGloballyPositioned { - endOffset = it.size.width - }) - } - } - } -} 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 new file mode 100644 index 0000000..6f3e8c7 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreview.kt @@ -0,0 +1,272 @@ +package net.newpipe.newplayer.ui.common.thumb_preview + +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + +import android.graphics.BitmapFactory +import androidx.annotation.OptIn +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +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.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.padding +import androidx.compose.foundation.layout.width +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 +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 +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.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 + +/** @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)) + + +/** @hide */ +@OptIn(UnstableApi::class) +@Composable +internal fun ThumbPreview( + modifier: Modifier = Modifier, + uiState: NewPlayerUIState, + thumbSize: Dp = SeekerDefaults.ThumbRadius * 2, + additionalStartPaddingPxls: Int = 0, + additionalEndPaddingPxls: Int = 0, + previewHeight: Dp = 60.dp, +) { + + Column(modifier = modifier) { + ThumbTextPreview( + modifier = Modifier, + uiState = uiState, + thumbSize = thumbSize, + startOffset = additionalStartPaddingPxls, + endOffset = additionalEndPaddingPxls, + previewHeight = previewHeight, + ) + + ThumbImagePreview( + modifier = Modifier, + uiState, + thumbSize, + additionalStartPaddingPxls, + additionalEndPaddingPxls, + previewHeight, + ) + } +} + +@OptIn(UnstableApi::class) +@Composable +private fun ThumbTextPreview( + modifier: Modifier = Modifier, + uiState: NewPlayerUIState, + thumbSize: Dp, + startOffset: Int, + endOffset: Int, + previewHeight: Dp, +) { + + val textHeight = 30.dp + + AnimatedVisibility( + modifier = modifier + .fillMaxWidth() + .height((2 * PREVIEW_BOX_PADDING).dp + textHeight), + visible = uiState.seekPreviewVisible && (uiState.currentSeekPreviewText ?: "") != "", + enter = PREVIEW_FADE_IN, + exit = PREVIEW_FADE_OUT + ) { + + PlaceRelativeToThumbSliderLayout( + 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, + 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, +) { + + AnimatedVisibility( + modifier = Modifier + .fillMaxWidth() + .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 + } + + lastAvailableImage?.let { lastAvailableImage -> + PlaceRelativeToThumbSliderLayout( + Modifier.wrapContentSize(), + uiState = uiState, + thumbSize = thumbSize, + startOffset = additionalStartPaddingPxls, + endOffset = additionalEndPaddingPxls + ) { + 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, + 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( + modifier = Modifier + .size(10.dp, 10.dp) + .offset { IntOffset(previewPosition.toInt(), 200) }, color = Color.Blue + ) { + } + */ + } +} + +@OptIn(UnstableApi::class) +@Preview(device = "spec:width=1080px,height=600px,dpi=440") +@Composable +private fun ThumbPreviewPreview() { + var sliderPosition by remember { mutableFloatStateOf(0f) } + + var startOffset by remember { mutableIntStateOf(0) } + var endOffset by remember { mutableIntStateOf(0) } + + var thumbDown by remember { mutableStateOf(false) } + + val previewThumbnail = + BitmapFactory.decodeResource(LocalContext.current.resources, R.mipmap.thumbnail_preview) + + + VideoPlayerTheme { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.Green) + ) { + ThumbPreview( + uiState = NewPlayerUIState.DUMMY.copy( + seekerPosition = sliderPosition, + seekPreviewVisible = thumbDown, + currentSeekPreviewThumbnail = previewThumbnail.asImageBitmap(), + currentSeekPreviewText = Chapter( + 0, "What a wonderfull chapter", null + ).chapterTitle + ), + additionalStartPaddingPxls = startOffset, + additionalEndPaddingPxls = endOffset, + thumbSize = 20.dp // see handle width + ) + + Row( + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "Left", modifier = Modifier.onGloballyPositioned { + startOffset = it.size.width + }) + Slider(modifier = Modifier.weight(1f), value = sliderPosition, onValueChange = { + thumbDown = true + sliderPosition = it + }, onValueChangeFinished = { thumbDown = false }) + Text(text = "R", modifier = Modifier.onGloballyPositioned { + endOffset = it.size.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..fb75d2d --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/common/thumb_preview/ThumbPreviewGeopmetryCalculations.kt @@ -0,0 +1,84 @@ +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 +import net.newpipe.newplayer.uiModel.NewPlayerUIState + +/** @hide */ +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 PlaceRelativeToThumbSliderLayout( + modifier: Modifier, + uiState: NewPlayerUIState, + thumbSize: Dp, + startOffset: Int, + endOffset: Int, + content: @Composable () -> Unit +) { + val thumbSizePxls = with(LocalDensity.current) { thumbSize.toPx() }.toInt() + + SubcomposeLayout(modifier) { 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() +} \ No newline at end of file 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..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 @@ -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,17 +43,21 @@ 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 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 @@ -62,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. * @@ -72,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) @@ -80,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) { @@ -108,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 @@ -118,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) @@ -131,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 @@ -154,8 +154,8 @@ internal fun Activity.getEmbeddedUiConfig(): EmbeddedUiConfig { ) } -@Composable /** @hide */ +@Composable internal fun getInsets() = WindowInsets.systemBars.union(WindowInsets.displayCutout).union(WindowInsets.waterfall) @@ -198,9 +198,8 @@ internal fun getTimeStringFromMs( return time_string } -@Composable - /** @hide */ +@Composable internal fun Thumbnail( modifier: Modifier = Modifier, thumbnail: Uri?, @@ -231,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) { @@ -266,4 +263,22 @@ internal fun relaunchCurrentActivity(activity: Activity) { */ activity.startActivity(activity.intent) +} + +/** @hide */ +@Composable +internal 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 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 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 )