mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Support for different seek bar implementations
This commit is contained in:
parent
9c95685385
commit
a7046a942e
5 changed files with 154 additions and 63 deletions
|
|
@ -97,7 +97,7 @@ fun PlaybackControls(
|
|||
controllerViewState: ControllerViewState,
|
||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||
showDebugInfo: Boolean,
|
||||
onSeekProgress: (Float) -> Unit,
|
||||
onSeekProgress: (Long) -> Unit,
|
||||
showPlay: Boolean,
|
||||
previousEnabled: Boolean,
|
||||
nextEnabled: Boolean,
|
||||
|
|
@ -194,7 +194,7 @@ fun SeekBar(
|
|||
isEnabled: Boolean,
|
||||
intervals: Int,
|
||||
controllerViewState: ControllerViewState,
|
||||
onSeekProgress: (Float) -> Unit,
|
||||
onSeekProgress: (Long) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
|
|
@ -213,17 +213,18 @@ fun SeekBar(
|
|||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SeekBarImpl(
|
||||
IntervalSeekBarImpl(
|
||||
progress = progress,
|
||||
bufferedProgress = bufferedProgress,
|
||||
onSeek = {
|
||||
onSeekProgress(it)
|
||||
},
|
||||
controllerViewState = controllerViewState,
|
||||
intervals = intervals,
|
||||
// intervals = intervals,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
interactionSource = interactionSource,
|
||||
enabled = isEnabled,
|
||||
durationMs = player.contentDuration,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -33,7 +33,7 @@ fun PlaybackOverlay(
|
|||
nextEnabled: Boolean,
|
||||
seekEnabled: Boolean,
|
||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||
onSeekBarChange: (Float) -> Unit,
|
||||
onSeekBarChange: (Long) -> Unit,
|
||||
showDebugInfo: Boolean,
|
||||
scale: ContentScale,
|
||||
playbackSpeed: Float,
|
||||
|
|
@ -48,8 +48,8 @@ fun PlaybackOverlay(
|
|||
seekBarInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
// Will be used for preview/trick play images
|
||||
var seekProgressPercent by remember { mutableFloatStateOf(-1f) }
|
||||
val seekProgressMs = seekProgressPercent * playerControls.duration
|
||||
var seekProgressMs by remember { mutableLongStateOf(-1L) }
|
||||
var seekProgressPercent = (seekProgressMs.toDouble() / playerControls.duration).toFloat()
|
||||
val seekBarFocused by seekBarInteractionSource.collectIsFocusedAsState()
|
||||
|
||||
Box(
|
||||
|
|
@ -97,7 +97,7 @@ fun PlaybackOverlay(
|
|||
),
|
||||
previewImageUrl = imageUrl,
|
||||
duration = playerControls.duration,
|
||||
seekProgress = seekProgressPercent,
|
||||
seekProgressMs = seekProgressMs,
|
||||
videoWidth = trickplayInfo.width,
|
||||
videoHeight = trickplayInfo.height,
|
||||
trickPlayInfo = trickplayInfo,
|
||||
|
|
@ -114,7 +114,7 @@ fun PlaybackOverlay(
|
|||
controllerViewState = controllerViewState,
|
||||
showDebugInfo = showDebugInfo,
|
||||
onSeekProgress = {
|
||||
seekProgressPercent = it
|
||||
seekProgressMs = it
|
||||
onSeekBarChange(it)
|
||||
},
|
||||
showPlay = showPlay,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.github.damontecres.dolphin.ui.playback
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.focusable
|
||||
|
|
@ -30,8 +31,10 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
|
@ -39,15 +42,21 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import com.github.damontecres.dolphin.ui.handleDPadKeyEvents
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun SeekBarImpl(
|
||||
fun SteppedSeekBarImpl(
|
||||
progress: Float,
|
||||
durationMs: Long,
|
||||
bufferedProgress: Float,
|
||||
onSeek: (seekProgress: Float) -> Unit,
|
||||
onSeek: (Long) -> Unit,
|
||||
controllerViewState: ControllerViewState,
|
||||
modifier: Modifier = Modifier,
|
||||
intervals: Int = 10,
|
||||
|
|
@ -55,10 +64,6 @@ fun SeekBarImpl(
|
|||
enabled: Boolean = true,
|
||||
) {
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
val color = MaterialTheme.colorScheme.border
|
||||
val animatedIndicatorHeight by animateDpAsState(
|
||||
targetValue = 6.dp.times((if (isFocused) 2f else 1f)),
|
||||
)
|
||||
var hasSeeked by remember { mutableStateOf(false) }
|
||||
var seekProgress by remember { mutableFloatStateOf(progress) }
|
||||
val progressToUse = if (isFocused && hasSeeked) seekProgress else progress
|
||||
|
|
@ -68,24 +73,90 @@ fun SeekBarImpl(
|
|||
|
||||
val offset = 1f / intervals
|
||||
|
||||
val handleSeekEventModifier =
|
||||
Modifier.handleDPadKeyEvents(
|
||||
onCenter = {
|
||||
controllerViewState.pulseControls()
|
||||
onSeek(seekProgress)
|
||||
},
|
||||
val seek = { percent: Float ->
|
||||
onSeek((percent * durationMs).toLong())
|
||||
}
|
||||
|
||||
SeekBarDisplay(
|
||||
enabled = enabled,
|
||||
progress = progressToUse,
|
||||
bufferedProgress = bufferedProgress,
|
||||
onLeft = {
|
||||
controllerViewState.pulseControls()
|
||||
seekProgress = (progressToUse - offset).coerceAtLeast(0f)
|
||||
hasSeeked = true
|
||||
onSeek(seekProgress)
|
||||
seek(seekProgress)
|
||||
},
|
||||
onRight = {
|
||||
controllerViewState.pulseControls()
|
||||
seekProgress = (progressToUse + offset).coerceAtMost(1f)
|
||||
hasSeeked = true
|
||||
onSeek(seekProgress)
|
||||
seek(seekProgress)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
fun IntervalSeekBarImpl(
|
||||
progress: Float,
|
||||
durationMs: Long,
|
||||
bufferedProgress: Float,
|
||||
onSeek: (Long) -> Unit,
|
||||
controllerViewState: ControllerViewState,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
var hasSeeked by remember { mutableStateOf(false) }
|
||||
var seekPositionMs by remember { mutableLongStateOf((progress * durationMs).toLong()) }
|
||||
val progressToUse by remember { derivedStateOf { if (isFocused && hasSeeked) seekPositionMs else (progress * durationMs).toLong() } }
|
||||
|
||||
LaunchedEffect(isFocused) {
|
||||
if (!isFocused) hasSeeked = false
|
||||
}
|
||||
|
||||
val offset = 30.seconds.inWholeMilliseconds
|
||||
|
||||
SeekBarDisplay(
|
||||
enabled = enabled,
|
||||
progress = (progressToUse.toDouble() / durationMs).toFloat(),
|
||||
bufferedProgress = bufferedProgress,
|
||||
onLeft = {
|
||||
controllerViewState.pulseControls()
|
||||
seekPositionMs = (progressToUse - offset).coerceAtLeast(0L)
|
||||
hasSeeked = true
|
||||
onSeek(seekPositionMs)
|
||||
},
|
||||
onRight = {
|
||||
controllerViewState.pulseControls()
|
||||
seekPositionMs = (progressToUse + offset).coerceAtMost(durationMs)
|
||||
hasSeeked = true
|
||||
onSeek(seekPositionMs)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SeekBarDisplay(
|
||||
progress: Float,
|
||||
bufferedProgress: Float,
|
||||
onLeft: () -> Unit,
|
||||
onRight: () -> Unit,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
val color = MaterialTheme.colorScheme.border
|
||||
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
val animatedIndicatorHeight by animateDpAsState(
|
||||
targetValue = 6.dp.times((if (isFocused) 2f else 1f)),
|
||||
)
|
||||
|
||||
Column(
|
||||
|
|
@ -98,8 +169,25 @@ fun SeekBarImpl(
|
|||
.fillMaxWidth()
|
||||
.height(animatedIndicatorHeight)
|
||||
.padding(horizontal = 4.dp)
|
||||
.then(handleSeekEventModifier)
|
||||
.focusable(enabled = enabled, interactionSource = interactionSource),
|
||||
.onPreviewKeyEvent { event ->
|
||||
val trigger =
|
||||
event.type == KeyEventType.KeyUp || event.nativeKeyEvent.repeatCount > 0
|
||||
when (event.nativeKeyEvent.keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> {
|
||||
if (trigger) onLeft.invoke()
|
||||
return@onPreviewKeyEvent true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> {
|
||||
if (trigger) onRight.invoke()
|
||||
return@onPreviewKeyEvent true
|
||||
}
|
||||
}
|
||||
false
|
||||
}.handleDPadKeyEvents(
|
||||
onLeft = onLeft,
|
||||
onRight = onRight,
|
||||
).focusable(enabled = enabled, interactionSource = interactionSource),
|
||||
onDraw = {
|
||||
val yOffset = size.height.div(2)
|
||||
drawLine(
|
||||
|
|
@ -126,7 +214,7 @@ fun SeekBarImpl(
|
|||
end =
|
||||
Offset(
|
||||
// x = size.width.times(if (isSelected) seekProgress else progress),
|
||||
x = size.width.times(progressToUse),
|
||||
x = size.width.times(progress),
|
||||
y = yOffset,
|
||||
),
|
||||
strokeWidth = size.height,
|
||||
|
|
@ -135,7 +223,7 @@ fun SeekBarImpl(
|
|||
drawCircle(
|
||||
color = Color.White,
|
||||
radius = size.height + 2,
|
||||
center = Offset(x = size.width.times(progressToUse), y = yOffset),
|
||||
center = Offset(x = size.width.times(progress), y = yOffset),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.listen
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
|
||||
@UnstableApi
|
||||
@Composable
|
||||
|
|
@ -37,23 +36,26 @@ class SeekBarState(
|
|||
var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_SEEK_FORWARD))
|
||||
private set
|
||||
|
||||
private var job: Job? = null
|
||||
private val channel = Channel<Long>(CONFLATED)
|
||||
|
||||
fun onValueChange(progress: Float) {
|
||||
job?.cancel()
|
||||
job =
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(750L)
|
||||
withContext(Dispatchers.Main) {
|
||||
player.seekTo((player.duration * progress).toLong())
|
||||
fun onValueChange(positionMs: Long) {
|
||||
channel.trySend(positionMs)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
suspend fun observe() {
|
||||
channel
|
||||
.consumeAsFlow()
|
||||
.debounce { 750L }
|
||||
.collect {
|
||||
player.seekTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun observe(): Nothing =
|
||||
player.listen { events ->
|
||||
if (events.contains(Player.EVENT_AVAILABLE_COMMANDS_CHANGED)) {
|
||||
isEnabled = isCommandAvailable(Player.COMMAND_SEEK_FORWARD)
|
||||
}
|
||||
}
|
||||
// suspend fun observe(): Nothing =
|
||||
// player.listen { events ->
|
||||
// if (events.contains(Player.EVENT_AVAILABLE_COMMANDS_CHANGED)) {
|
||||
// isEnabled = isCommandAvailable(Player.COMMAND_SEEK_FORWARD)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ fun Modifier.offsetByPercent(xPercentage: Float) =
|
|||
fun SeekPreviewImage(
|
||||
previewImageUrl: String,
|
||||
duration: Long,
|
||||
seekProgress: Float,
|
||||
seekProgressMs: Long,
|
||||
videoWidth: Int?,
|
||||
videoHeight: Int?,
|
||||
trickPlayInfo: TrickplayInfo,
|
||||
|
|
@ -86,7 +86,7 @@ fun SeekPreviewImage(
|
|||
val heightPx = with(LocalDensity.current) { height.toPx().toInt() }
|
||||
val widthPx = with(LocalDensity.current) { width.toPx().toInt() }
|
||||
|
||||
val index = (duration * seekProgress / trickPlayInfo.interval).toInt() // Which tile
|
||||
val index = (seekProgressMs.toDouble() / trickPlayInfo.interval).toInt() // Which tile
|
||||
val numberOfTitlesPerImage = trickPlayInfo.tileHeight * trickPlayInfo.tileWidth
|
||||
val imageIndex = index % numberOfTitlesPerImage
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ fun SeekPreviewImage(
|
|||
)
|
||||
}
|
||||
Text(
|
||||
text = (seekProgress * duration / 1000).toLong().seconds.toString(),
|
||||
text = (seekProgressMs / 1000L).seconds.toString(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue