make chapters visible in the seekbar
This commit is contained in:
parent
8a3cc12bd7
commit
430bed20b6
|
@ -81,7 +81,7 @@ data class VideoPlayerUIState(
|
||||||
seekerPosition = 0.3f,
|
seekerPosition = 0.3f,
|
||||||
bufferedPercentage = 0.5f,
|
bufferedPercentage = 0.5f,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
durationInMs = 420,
|
durationInMs = 12000,
|
||||||
playbackPositionInPlaylistS = 5039,
|
playbackPositionInPlaylistS = 5039,
|
||||||
playbackPositionInMs = 69,
|
playbackPositionInMs = 69,
|
||||||
fastSeekSeconds = 10,
|
fastSeekSeconds = 10,
|
||||||
|
@ -92,6 +92,11 @@ data class VideoPlayerUIState(
|
||||||
currentlyPlaying = PlaylistItem.DUMMY,
|
currentlyPlaying = PlaylistItem.DUMMY,
|
||||||
currentPlaylistItemIndex = 1,
|
currentPlaylistItemIndex = 1,
|
||||||
chapters = arrayListOf(
|
chapters = arrayListOf(
|
||||||
|
Chapter(
|
||||||
|
chapterStartInMs = 0,
|
||||||
|
chapterTitle = "Intro",
|
||||||
|
thumbnail = null
|
||||||
|
),
|
||||||
Chapter(
|
Chapter(
|
||||||
chapterStartInMs = 5000,
|
chapterStartInMs = 5000,
|
||||||
chapterTitle = "First Chapter",
|
chapterTitle = "First Chapter",
|
||||||
|
@ -102,11 +107,6 @@ data class VideoPlayerUIState(
|
||||||
chapterTitle = "Second Chapter",
|
chapterTitle = "Second Chapter",
|
||||||
thumbnail = null
|
thumbnail = null
|
||||||
),
|
),
|
||||||
Chapter(
|
|
||||||
chapterStartInMs = 20000,
|
|
||||||
chapterTitle = "Third Chapter",
|
|
||||||
thumbnail = null
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
playList = arrayListOf(
|
playList = arrayListOf(
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
|
|
|
@ -42,7 +42,7 @@ interface VideoPlayerViewModel {
|
||||||
fun pause()
|
fun pause()
|
||||||
fun prevStream()
|
fun prevStream()
|
||||||
fun nextStream()
|
fun nextStream()
|
||||||
fun switchToFullscreen(embeddedUiConfig: EmbeddedUiConfig)
|
fun switchToFullscreen()
|
||||||
fun switchToEmbeddedView()
|
fun switchToEmbeddedView()
|
||||||
fun onBackPressed()
|
fun onBackPressed()
|
||||||
fun showUi()
|
fun showUi()
|
||||||
|
@ -54,7 +54,7 @@ interface VideoPlayerViewModel {
|
||||||
fun finishFastSeek()
|
fun finishFastSeek()
|
||||||
fun brightnessChange(changeRate: Float, systemBrightness: Float)
|
fun brightnessChange(changeRate: Float, systemBrightness: Float)
|
||||||
fun volumeChange(changeRate: Float)
|
fun volumeChange(changeRate: Float)
|
||||||
fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig)
|
fun openStreamSelection(selectChapter: Boolean)
|
||||||
fun closeStreamSelection()
|
fun closeStreamSelection()
|
||||||
fun chapterSelected(chapterId: Int)
|
fun chapterSelected(chapterId: Int)
|
||||||
fun streamSelected(streamId: Int)
|
fun streamSelected(streamId: Int)
|
||||||
|
|
|
@ -432,11 +432,8 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig) {
|
override fun openStreamSelection(selectChapter: Boolean) {
|
||||||
uiVisibilityJob?.cancel()
|
uiVisibilityJob?.cancel()
|
||||||
if (!uiState.value.uiMode.fullscreen) {
|
|
||||||
this.embeddedUiConfig = embeddedUiConfig
|
|
||||||
}
|
|
||||||
updateUiMode(
|
updateUiMode(
|
||||||
if (selectChapter) uiState.value.uiMode.getChapterSelectUiState()
|
if (selectChapter) uiState.value.uiMode.getChapterSelectUiState()
|
||||||
else uiState.value.uiMode.getStreamSelectUiState()
|
else uiState.value.uiMode.getStreamSelectUiState()
|
||||||
|
@ -469,11 +466,10 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun switchToFullscreen(embeddedUiConfig: EmbeddedUiConfig) {
|
override fun switchToFullscreen() {
|
||||||
uiVisibilityJob?.cancel()
|
uiVisibilityJob?.cancel()
|
||||||
finishFastSeek()
|
finishFastSeek()
|
||||||
|
|
||||||
this.embeddedUiConfig = embeddedUiConfig
|
|
||||||
updateUiMode(UIModeState.FULLSCREEN_VIDEO)
|
updateUiMode(UIModeState.FULLSCREEN_VIDEO)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
|
||||||
println("dummy impl")
|
println("dummy impl")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun switchToFullscreen(embeddedUiConfig: EmbeddedUiConfig) {
|
override fun switchToFullscreen() {
|
||||||
println("dummy impl")
|
println("dummy impl")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fastSeek(steps: Int) {
|
override fun fastSeek(steps: Int) {
|
||||||
println("dummy impl")
|
println("dummy impl, steps: $steps")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finishFastSeek() {
|
override fun finishFastSeek() {
|
||||||
|
@ -76,7 +76,7 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
|
||||||
println("dummy impl")
|
println("dummy impl")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig) {
|
override fun openStreamSelection(selectChapter: Boolean) {
|
||||||
println("dummy impl")
|
println("dummy impl")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,11 +36,8 @@ import androidx.compose.foundation.interaction.Interaction
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.requiredSizeIn
|
import androidx.compose.foundation.layout.requiredSizeIn
|
||||||
|
@ -49,8 +46,6 @@ import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.progressSemantics
|
import androidx.compose.foundation.progressSemantics
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -75,7 +70,6 @@ import androidx.compose.ui.graphics.drawscope.rotateRad
|
||||||
import androidx.compose.ui.graphics.drawscope.translate
|
import androidx.compose.ui.graphics.drawscope.translate
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.VerticalAlignmentLine
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.semantics.disabled
|
import androidx.compose.ui.semantics.disabled
|
||||||
|
@ -135,18 +129,25 @@ fun Seeker(
|
||||||
onValueChange: (Float) -> Unit,
|
onValueChange: (Float) -> Unit,
|
||||||
onValueChangeFinished: (() -> Unit)? = null,
|
onValueChangeFinished: (() -> Unit)? = null,
|
||||||
segments: List<Segment> = emptyList(),
|
segments: List<Segment> = emptyList(),
|
||||||
|
chapterSegments: List<ChapterSegment> = emptyList(),
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
colors: SeekerColors = SeekerDefaults.seekerColors(),
|
colors: SeekerColors = SeekerDefaults.seekerColors(),
|
||||||
dimensions: SeekerDimensions = SeekerDefaults.seekerDimensions(),
|
dimensions: SeekerDimensions = SeekerDefaults.seekerDimensions(),
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
) {
|
) {
|
||||||
if (segments.isNotEmpty()) {
|
if (segments.isNotEmpty()) {
|
||||||
require(segments.first().start == range.start) {
|
|
||||||
"the first segment should start from range start value"
|
|
||||||
}
|
|
||||||
segments.forEach {
|
segments.forEach {
|
||||||
|
require(it.start in range && it.end in range) {
|
||||||
|
"segment must lie withing the range: segment: ${it.name} start: ${it.start}, end: ${it.end}, range: ${range}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (chapterSegments.isNotEmpty()) {
|
||||||
|
chapterSegments.forEach {
|
||||||
require(it.start in range) {
|
require(it.start in range) {
|
||||||
"segment must start from withing the range."
|
"chapter segment must lie withing the range: segment: ${it.name} start: ${it.start} range: ${range}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,6 +179,10 @@ fun Seeker(
|
||||||
segmentToPxValues(segments, range, widthPx)
|
segmentToPxValues(segments, range, widthPx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val chapterSegmentsPx = remember(chapterSegments, range, widthPx) {
|
||||||
|
chapterSegmentToPxValues(chapterSegments, range, widthPx)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(thumbValue, segments) {
|
LaunchedEffect(thumbValue, segments) {
|
||||||
state.currentSegment(thumbValue, segments)
|
state.currentSegment(thumbValue, segments)
|
||||||
}
|
}
|
||||||
|
@ -259,6 +264,7 @@ fun Seeker(
|
||||||
readAheadValuePx = readAheadValuePx,
|
readAheadValuePx = readAheadValuePx,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
segments = segmentStarts,
|
segments = segmentStarts,
|
||||||
|
chapterSegments = chapterSegmentsPx,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
dimensions = dimensions,
|
dimensions = dimensions,
|
||||||
interactionSource = interactionSource
|
interactionSource = interactionSource
|
||||||
|
@ -276,6 +282,7 @@ private fun Seeker(
|
||||||
readAheadValuePx: Float,
|
readAheadValuePx: Float,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
segments: List<SegmentPxs>,
|
segments: List<SegmentPxs>,
|
||||||
|
chapterSegments: List<SegmentPxs>,
|
||||||
colors: SeekerColors,
|
colors: SeekerColors,
|
||||||
dimensions: SeekerDimensions,
|
dimensions: SeekerDimensions,
|
||||||
interactionSource: MutableInteractionSource
|
interactionSource: MutableInteractionSource
|
||||||
|
@ -288,6 +295,7 @@ private fun Seeker(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
segments = segments,
|
segments = segments,
|
||||||
|
chapterSegments = chapterSegments,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
widthPx = widthPx,
|
widthPx = widthPx,
|
||||||
valuePx = valuePx,
|
valuePx = valuePx,
|
||||||
|
@ -310,6 +318,7 @@ private fun Track(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
segments: List<SegmentPxs>,
|
segments: List<SegmentPxs>,
|
||||||
|
chapterSegments: List<SegmentPxs>,
|
||||||
colors: SeekerColors,
|
colors: SeekerColors,
|
||||||
widthPx: Float,
|
widthPx: Float,
|
||||||
valuePx: Float,
|
valuePx: Float,
|
||||||
|
@ -334,7 +343,7 @@ private fun Track(
|
||||||
val left = thumbRadius.toPx()
|
val left = thumbRadius.toPx()
|
||||||
|
|
||||||
translate(left = left) {
|
translate(left = left) {
|
||||||
if (segments.isEmpty()) {
|
|
||||||
// draw the track with a single line.
|
// draw the track with a single line.
|
||||||
drawLine(
|
drawLine(
|
||||||
start = Offset(rtlAware(0f, widthPx, isRtl), center.y),
|
start = Offset(rtlAware(0f, widthPx, isRtl), center.y),
|
||||||
|
@ -343,26 +352,7 @@ private fun Track(
|
||||||
strokeWidth = trackHeight.toPx(),
|
strokeWidth = trackHeight.toPx(),
|
||||||
cap = StrokeCap.Round
|
cap = StrokeCap.Round
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
// draw segments in their respective color,
|
|
||||||
// excluding gaps (which will be cleared out later)
|
|
||||||
for (index in segments.indices) {
|
|
||||||
val segment = segments[index]
|
|
||||||
val segmentColor = when (segment.color) {
|
|
||||||
Color.Unspecified -> trackColor
|
|
||||||
else -> segment.color
|
|
||||||
}
|
|
||||||
drawSegment(
|
|
||||||
startPx = rtlAware(segment.startPx, widthPx, isRtl),
|
|
||||||
endPx = rtlAware(segment.endPx, widthPx, isRtl),
|
|
||||||
trackColor = segmentColor,
|
|
||||||
trackHeight = trackHeight.toPx(),
|
|
||||||
blendMode = BlendMode.SrcOver,
|
|
||||||
startCap = if (index == 0) StrokeCap.Round else null,
|
|
||||||
endCap = if (index == segments.lastIndex) StrokeCap.Round else null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readAhead indicator
|
// readAhead indicator
|
||||||
drawLine(
|
drawLine(
|
||||||
|
@ -382,14 +372,28 @@ private fun Track(
|
||||||
cap = StrokeCap.Round
|
cap = StrokeCap.Round
|
||||||
)
|
)
|
||||||
|
|
||||||
// clear segment gaps
|
// draw segments in their respective color,
|
||||||
for (index in segments.indices) {
|
for (index in segments.indices) {
|
||||||
if (index == segments.lastIndex) break // skip "gap" after last segment
|
|
||||||
val segment = segments[index]
|
val segment = segments[index]
|
||||||
drawGap(
|
val segmentColor = when (segment.color) {
|
||||||
startPx = rtlAware(segment.endPx - segmentGap.toPx(), widthPx, isRtl),
|
Color.Unspecified -> trackColor
|
||||||
|
else -> segment.color
|
||||||
|
}
|
||||||
|
drawSegment(
|
||||||
|
startPx = rtlAware(segment.startPx, widthPx, isRtl),
|
||||||
endPx = rtlAware(segment.endPx, widthPx, isRtl),
|
endPx = rtlAware(segment.endPx, widthPx, isRtl),
|
||||||
|
trackColor = segmentColor,
|
||||||
trackHeight = trackHeight.toPx(),
|
trackHeight = trackHeight.toPx(),
|
||||||
|
blendMode = BlendMode.SrcOver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear segment gaps
|
||||||
|
for (index in chapterSegments.indices) {
|
||||||
|
val segment = chapterSegments[index]
|
||||||
|
drawDot(
|
||||||
|
x = rtlAware(segment.startPx, widthPx, isRtl),
|
||||||
|
trackHeight = trackHeight.toPx()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -485,8 +489,6 @@ private fun DrawScope.drawSegment(
|
||||||
trackColor: Color,
|
trackColor: Color,
|
||||||
trackHeight: Float,
|
trackHeight: Float,
|
||||||
blendMode: BlendMode,
|
blendMode: BlendMode,
|
||||||
startCap: StrokeCap? = null,
|
|
||||||
endCap: StrokeCap? = null
|
|
||||||
) {
|
) {
|
||||||
drawLine(
|
drawLine(
|
||||||
start = Offset(startPx, center.y),
|
start = Offset(startPx, center.y),
|
||||||
|
@ -494,8 +496,7 @@ private fun DrawScope.drawSegment(
|
||||||
color = trackColor,
|
color = trackColor,
|
||||||
strokeWidth = trackHeight,
|
strokeWidth = trackHeight,
|
||||||
blendMode = blendMode,
|
blendMode = blendMode,
|
||||||
endCap = endCap,
|
cap = StrokeCap.Round
|
||||||
startCap = startCap
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -513,6 +514,18 @@ private fun DrawScope.drawGap(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun DrawScope.drawDot(
|
||||||
|
x: Float,
|
||||||
|
trackHeight: Float
|
||||||
|
) {
|
||||||
|
drawCircle(
|
||||||
|
radius = (trackHeight / 2f) * 0.8f,
|
||||||
|
center = Offset(x = x, y = center.y),
|
||||||
|
color = Color.Gray.copy(alpha = 0.9f),
|
||||||
|
blendMode = BlendMode.SrcOver
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Thumb(
|
private fun Thumb(
|
||||||
valuePx: () -> Float,
|
valuePx: () -> Float,
|
||||||
|
@ -580,14 +593,21 @@ private fun Modifier.progressSemantics(
|
||||||
@Composable
|
@Composable
|
||||||
fun SeekerPreview() {
|
fun SeekerPreview() {
|
||||||
val segments = listOf(
|
val segments = listOf(
|
||||||
Segment(name = "Intro", start = 0f),
|
Segment(name = "Intro", start = 0.1f, end = 0.3f, color = Color.Green),
|
||||||
Segment(name = "Talk 1", start = 0.5f),
|
Segment(name = "Talk 1", start = 0.5f, end = 0.6f, color = Color.Cyan),
|
||||||
Segment(name = "Talk 2", start = 0.8f),
|
Segment(name = "Talk 2", start = 0.8f, end = 0.85f, color = Color.Blue),
|
||||||
|
)
|
||||||
|
|
||||||
|
val chapterSegments = listOf(
|
||||||
|
ChapterSegment(name = "Intro", start = 0.0f, color = Color.Green),
|
||||||
|
ChapterSegment(name = "Talk 1", start = 0.55f, color = Color.Cyan),
|
||||||
|
ChapterSegment(name = "Talk 2", start = 0.9f, color = Color.Blue),
|
||||||
)
|
)
|
||||||
Seeker(
|
Seeker(
|
||||||
value = 0.7f,
|
value = 0.7f,
|
||||||
range = 0f..1f,
|
range = 0f..1f,
|
||||||
segments = segments,
|
segments = segments,
|
||||||
|
chapterSegments = chapterSegments,
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -74,13 +74,25 @@ fun rememberSeekerState(): SeekerState = remember {
|
||||||
data class Segment(
|
data class Segment(
|
||||||
val name: String,
|
val name: String,
|
||||||
val start: Float,
|
val start: Float,
|
||||||
|
val end: Float,
|
||||||
val color: Color = Color.Unspecified
|
val color: Color = Color.Unspecified
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Unspecified = Segment(name = "", start = 0f)
|
val Unspecified = Segment(name = "", start = 0f, end = 0f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ChapterSegment(
|
||||||
|
val name: String,
|
||||||
|
val start: Float,
|
||||||
|
val color: Color = Color.Unspecified
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val Unspecified = ChapterSegment(name = "", start = 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
internal data class SegmentPxs(
|
internal data class SegmentPxs(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
|
@ -57,6 +57,37 @@ internal fun segmentToPxValues(
|
||||||
|
|
||||||
val rangeSize = range.endInclusive - range.start
|
val rangeSize = range.endInclusive - range.start
|
||||||
val sortedSegments = segments.distinct().sortedBy { it.start }
|
val sortedSegments = segments.distinct().sortedBy { it.start }
|
||||||
|
val segmentRangesPxs = sortedSegments.map { segment ->
|
||||||
|
|
||||||
|
// percent of the start of this segment in the range size
|
||||||
|
val percentStart = (segment.start - range.start) * 100 / rangeSize
|
||||||
|
val percentEnd = (segment.end - range.start) * 100 / rangeSize
|
||||||
|
val startPx = percentStart * widthPx / 100
|
||||||
|
val endPx = percentEnd * widthPx / 100
|
||||||
|
Pair(startPx, endPx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedSegments.mapIndexed { index, segment ->
|
||||||
|
SegmentPxs(
|
||||||
|
name = segment.name,
|
||||||
|
color = segment.color,
|
||||||
|
startPx = segmentRangesPxs[index].first,
|
||||||
|
endPx = segmentRangesPxs[index].second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun chapterSegmentToPxValues(
|
||||||
|
segments: List<ChapterSegment>,
|
||||||
|
range: ClosedFloatingPointRange<Float>,
|
||||||
|
widthPx: Float,
|
||||||
|
): List<SegmentPxs> {
|
||||||
|
|
||||||
|
val rangeSize = range.endInclusive - range.start
|
||||||
|
val sortedSegments = ArrayList(segments.distinct().sortedBy { it.start })
|
||||||
|
if(sortedSegments.isNotEmpty()) {
|
||||||
|
sortedSegments.removeAt(0)
|
||||||
|
}
|
||||||
val segmentStartPxs = sortedSegments.map { segment ->
|
val segmentStartPxs = sortedSegments.map { segment ->
|
||||||
|
|
||||||
// percent of the start of this segment in the range size
|
// percent of the start of this segment in the range size
|
||||||
|
|
|
@ -23,6 +23,7 @@ package net.newpipe.newplayer.ui.videoplayer
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.LocaleConfig
|
import android.app.LocaleConfig
|
||||||
import android.icu.text.DecimalFormat
|
import android.icu.text.DecimalFormat
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
@ -44,21 +45,23 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import net.newpipe.newplayer.Chapter
|
||||||
import net.newpipe.newplayer.R
|
import net.newpipe.newplayer.R
|
||||||
import net.newpipe.newplayer.model.UIModeState
|
import net.newpipe.newplayer.model.UIModeState
|
||||||
import net.newpipe.newplayer.model.VideoPlayerUIState
|
import net.newpipe.newplayer.model.VideoPlayerUIState
|
||||||
import net.newpipe.newplayer.model.VideoPlayerViewModel
|
import net.newpipe.newplayer.model.VideoPlayerViewModel
|
||||||
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
|
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
|
||||||
|
import net.newpipe.newplayer.ui.seeker.ChapterSegment
|
||||||
import net.newpipe.newplayer.ui.seeker.DefaultSeekerColor
|
import net.newpipe.newplayer.ui.seeker.DefaultSeekerColor
|
||||||
import net.newpipe.newplayer.ui.seeker.Seeker
|
import net.newpipe.newplayer.ui.seeker.Seeker
|
||||||
import net.newpipe.newplayer.ui.seeker.SeekerColors
|
import net.newpipe.newplayer.ui.seeker.SeekerColors
|
||||||
import net.newpipe.newplayer.ui.seeker.SeekerDefaults
|
import net.newpipe.newplayer.ui.seeker.Segment
|
||||||
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
import net.newpipe.newplayer.utils.getEmbeddedUiConfig
|
|
||||||
import net.newpipe.newplayer.utils.getLocale
|
import net.newpipe.newplayer.utils.getLocale
|
||||||
import net.newpipe.newplayer.utils.getTimeStringFromMs
|
import net.newpipe.newplayer.utils.getTimeStringFromMs
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.math.min
|
|
||||||
|
private const val TAG = "BottomUI"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomUI(
|
fun BottomUI(
|
||||||
|
@ -77,17 +80,17 @@ fun BottomUI(
|
||||||
onValueChange = viewModel::seekPositionChanged,
|
onValueChange = viewModel::seekPositionChanged,
|
||||||
onValueChangeFinished = viewModel::seekingFinished,
|
onValueChangeFinished = viewModel::seekingFinished,
|
||||||
readAheadValue = uiState.bufferedPercentage,
|
readAheadValue = uiState.bufferedPercentage,
|
||||||
colors = customizedSeekerColors()
|
colors = customizedSeekerColors(),
|
||||||
|
chapterSegments = getSeekerSegmentsFromChapters(uiState.chapters, uiState.durationInMs)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: locale))
|
Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: locale))
|
||||||
|
|
||||||
val embeddedUiConfig = getEmbeddedUiConfig(LocalContext.current as Activity)
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = if (uiState.uiMode.fullscreen) viewModel::switchToEmbeddedView
|
onClick = if (uiState.uiMode.fullscreen) viewModel::switchToEmbeddedView
|
||||||
else {
|
else {
|
||||||
{ // <- head of lambda ... yea kotlin is weird
|
{ // <- head of lambda ... yea kotlin is weird
|
||||||
viewModel.switchToFullscreen(embeddedUiConfig)
|
viewModel.switchToFullscreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
@ -114,6 +117,21 @@ private fun customizedSeekerColors(): SeekerColors {
|
||||||
return colors
|
return colors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getSeekerSegmentsFromChapters(chapters: List<Chapter>, duration: Long) =
|
||||||
|
chapters.map { chapter ->
|
||||||
|
val markPosition = chapter.chapterStartInMs.toFloat() / duration.toFloat()
|
||||||
|
if (markPosition < 0f || 1f < markPosition) {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"Chapter mark outside of stream duration range: chapter: ${chapter.chapterTitle}, mark in ms: ${chapter.chapterStartInMs}, vidoe duration in ms: ${duration}"
|
||||||
|
)
|
||||||
|
ChapterSegment(name = chapter.chapterTitle ?: "", start = 0f)
|
||||||
|
} else {
|
||||||
|
ChapterSegment(name = chapter.chapterTitle ?: "", start = markPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////
|
||||||
// Preview
|
// Preview
|
||||||
///////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -51,16 +51,14 @@ import net.newpipe.newplayer.R
|
||||||
import net.newpipe.newplayer.model.VideoPlayerUIState
|
import net.newpipe.newplayer.model.VideoPlayerUIState
|
||||||
import net.newpipe.newplayer.model.VideoPlayerViewModel
|
import net.newpipe.newplayer.model.VideoPlayerViewModel
|
||||||
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
|
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
|
||||||
import net.newpipe.newplayer.playerInternals.PlaylistItem
|
|
||||||
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
import net.newpipe.newplayer.ui.theme.video_player_onSurface
|
import net.newpipe.newplayer.ui.theme.video_player_onSurface
|
||||||
import net.newpipe.newplayer.utils.getEmbeddedUiConfig
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopUI(
|
fun TopUI(
|
||||||
modifier: Modifier, viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState
|
modifier: Modifier, viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState
|
||||||
) {
|
) {
|
||||||
val embeddedUiConfig = getEmbeddedUiConfig(activity = LocalContext.current as Activity)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
@ -101,7 +99,7 @@ fun TopUI(
|
||||||
}
|
}
|
||||||
AnimatedVisibility(visible = uiState.chapters.isNotEmpty()) {
|
AnimatedVisibility(visible = uiState.chapters.isNotEmpty()) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.openStreamSelection(selectChapter = true, embeddedUiConfig) },
|
onClick = { viewModel.openStreamSelection(selectChapter = true) },
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.MenuBook,
|
imageVector = Icons.AutoMirrored.Filled.MenuBook,
|
||||||
|
@ -113,8 +111,7 @@ fun TopUI(
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.openStreamSelection(
|
viewModel.openStreamSelection(
|
||||||
selectChapter = false,
|
selectChapter = false
|
||||||
embeddedUiConfig
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -21,8 +21,7 @@
|
||||||
|
|
||||||
package net.newpipe.newplayer.ui.videoplayer.gesture_ui
|
package net.newpipe.newplayer.ui.videoplayer.gesture_ui
|
||||||
|
|
||||||
import android.app.Activity
|
import android.util.Log
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
@ -36,13 +35,11 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import net.newpipe.newplayer.model.VideoPlayerUIState
|
import net.newpipe.newplayer.model.VideoPlayerUIState
|
||||||
import net.newpipe.newplayer.model.VideoPlayerViewModel
|
import net.newpipe.newplayer.model.VideoPlayerViewModel
|
||||||
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
|
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
|
||||||
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
import net.newpipe.newplayer.utils.getEmbeddedUiConfig
|
|
||||||
|
|
||||||
private const val TAG = "EmbeddedGestureUI"
|
private const val TAG = "EmbeddedGestureUI"
|
||||||
|
|
||||||
|
@ -55,8 +52,6 @@ fun EmbeddedGestureUI(
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val embeddedUiConfig = getEmbeddedUiConfig(LocalContext.current as Activity)
|
|
||||||
|
|
||||||
val handleMovement = { movement: TouchedPosition ->
|
val handleMovement = { movement: TouchedPosition ->
|
||||||
Log.d(TAG, "${movement.x}:${movement.y}")
|
Log.d(TAG, "${movement.x}:${movement.y}")
|
||||||
if (0 < movement.y) {
|
if (0 < movement.y) {
|
||||||
|
@ -66,7 +61,7 @@ fun EmbeddedGestureUI(
|
||||||
|
|
||||||
// this check is there to allow a temporary move up in the downward gesture
|
// this check is there to allow a temporary move up in the downward gesture
|
||||||
if (downwardMovementMode == false) {
|
if (downwardMovementMode == false) {
|
||||||
viewModel.switchToFullscreen(embeddedUiConfig)
|
viewModel.switchToFullscreen()
|
||||||
} else {
|
} else {
|
||||||
viewModel.embeddedDraggedDown(movement.y)
|
viewModel.embeddedDraggedDown(movement.y)
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,25 +89,6 @@ fun getLocale(): Locale? {
|
||||||
return ConfigurationCompat.getLocales(configuration).get(0)
|
return ConfigurationCompat.getLocales(configuration).get(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
@ReadOnlyComposable
|
|
||||||
fun getEmbeddedUiConfig(activity: Activity): EmbeddedUiConfig {
|
|
||||||
val window = activity.window
|
|
||||||
val view = LocalView.current
|
|
||||||
|
|
||||||
val isLightStatusBar = WindowCompat.getInsetsController(
|
|
||||||
window,
|
|
||||||
view
|
|
||||||
).isAppearanceLightStatusBars
|
|
||||||
val screenOrientation = activity.requestedOrientation
|
|
||||||
val defaultBrightness = getDefaultBrightness(activity)
|
|
||||||
return EmbeddedUiConfig(
|
|
||||||
systemBarInLightMode = isLightStatusBar,
|
|
||||||
brightness = defaultBrightness,
|
|
||||||
screenOrientation = screenOrientation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getInsets() =
|
fun getInsets() =
|
||||||
WindowInsets.systemBars.union(WindowInsets.displayCutout).union(WindowInsets.waterfall)
|
WindowInsets.systemBars.union(WindowInsets.displayCutout).union(WindowInsets.waterfall)
|
||||||
|
|
Loading…
Reference in New Issue