highlight active chapter

This commit is contained in:
Christian Schabesberger 2024-09-06 13:58:52 +02:00
parent 48f5e159d9
commit 79f8719ac3
6 changed files with 78 additions and 39 deletions

View File

@ -21,14 +21,11 @@
package net.newpipe.newplayer.model package net.newpipe.newplayer.model
import android.os.Bundle import android.os.Bundle
import androidx.media3.common.Player
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
import net.newpipe.newplayer.utils.Thumbnail
interface VideoPlayerViewModel { interface VideoPlayerViewModel {
@ -61,7 +58,7 @@ interface VideoPlayerViewModel {
fun closeStreamSelection() fun closeStreamSelection()
fun chapterSelected(chapter: Chapter) fun chapterSelected(chapter: Chapter)
fun streamSelected(streamId: Int) fun streamSelected(streamId: Int)
fun cycleRepeatmode() fun cycleRepeatMode()
fun toggleShuffle() fun toggleShuffle()
fun onStorePlaylist() fun onStorePlaylist()
fun movePlaylistItem(from: Int, to: Int) fun movePlaylistItem(from: Int, to: Int)

View File

@ -419,7 +419,6 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
override fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig) { override fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig) {
resetPlaylistProgressUpdaterJob()
uiVisibilityJob?.cancel() uiVisibilityJob?.cancel()
if (!uiState.value.uiMode.fullscreen) { if (!uiState.value.uiMode.fullscreen) {
this.embeddedUiConfig = embeddedUiConfig this.embeddedUiConfig = embeddedUiConfig
@ -428,10 +427,16 @@ class VideoPlayerViewModelImpl @Inject constructor(
if (selectChapter) uiState.value.uiMode.getChapterSelectUiState() if (selectChapter) uiState.value.uiMode.getChapterSelectUiState()
else uiState.value.uiMode.getStreamSelectUiState() else uiState.value.uiMode.getStreamSelectUiState()
) )
if(selectChapter) {
resetProgressUpdatePeriodicallyJob()
} else {
resetPlaylistProgressUpdaterJob()
}
} }
override fun closeStreamSelection() { override fun closeStreamSelection() {
playlistProgressUpdatrJob?.cancel() playlistProgressUpdatrJob?.cancel()
progressUpdaterJob?.cancel()
updateUiMode(uiState.value.uiMode.getUiHiddenState()) updateUiMode(uiState.value.uiMode.getUiHiddenState())
} }
@ -466,7 +471,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
println("stream selected: $streamId") println("stream selected: $streamId")
} }
override fun cycleRepeatmode() { override fun cycleRepeatMode() {
newPlayer?.let { newPlayer?.let {
it.repeatMode = when (it.repeatMode) { it.repeatMode = when (it.repeatMode) {
RepeatMode.DONT_REPEAT -> RepeatMode.REPEAT_ALL RepeatMode.DONT_REPEAT -> RepeatMode.REPEAT_ALL

View File

@ -92,7 +92,7 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
println("dummy impl stream selected: $streamId") println("dummy impl stream selected: $streamId")
} }
override fun cycleRepeatmode() { override fun cycleRepeatMode() {
println("dummy impl") println("dummy impl")
} }

View File

@ -48,6 +48,7 @@ import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar
import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamItem import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamItem
import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamSelectTopBar import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamSelectTopBar
import net.newpipe.newplayer.ui.videoplayer.streamselect.isActiveChapter
import net.newpipe.newplayer.utils.ReorderHapticFeedbackType import net.newpipe.newplayer.utils.ReorderHapticFeedbackType
import net.newpipe.newplayer.utils.getInsets import net.newpipe.newplayer.utils.getInsets
import net.newpipe.newplayer.utils.rememberReorderHapticFeedback import net.newpipe.newplayer.utils.rememberReorderHapticFeedback
@ -100,7 +101,12 @@ fun StreamSelectUI(
thumbnail = chapter.thumbnail, thumbnail = chapter.thumbnail,
onClicked = { onClicked = {
viewModel.chapterSelected(chapter) viewModel.chapterSelected(chapter)
} },
isCurrentChapter = isActiveChapter(
chapterIndex,
uiState.chapters,
uiState.playbackPositionInMs
)
) )
} }
@ -139,7 +145,7 @@ fun ReorderableStreamItemsList(
verticalArrangement = Arrangement.spacedBy(5.dp), verticalArrangement = Arrangement.spacedBy(5.dp),
state = lazyListState state = lazyListState
) { ) {
itemsIndexed(uiState.playList, key = {_, item -> item.uniqueId}) { index, playlistItem -> itemsIndexed(uiState.playList, key = { _, item -> item.uniqueId }) { index, playlistItem ->
ReorderableItem( ReorderableItem(
state = reorderableLazyListState, state = reorderableLazyListState,
key = playlistItem.uniqueId key = playlistItem.uniqueId

View File

@ -21,14 +21,20 @@
package net.newpipe.newplayer.ui.videoplayer.streamselect package net.newpipe.newplayer.ui.videoplayer.streamselect
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.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -44,6 +50,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.videoplayer.ITEM_CORNER_SHAPE import net.newpipe.newplayer.ui.videoplayer.ITEM_CORNER_SHAPE
@ -54,6 +62,16 @@ import net.newpipe.newplayer.utils.VectorThumbnail
import net.newpipe.newplayer.utils.getLocale import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs import net.newpipe.newplayer.utils.getTimeStringFromMs
fun isActiveChapter(chapterId: Int, chapters: List<Chapter>, playbackPosition: Long) : Boolean {
assert(0 <= chapterId && chapterId < chapters.size) {
throw NewPlayerException("Chapter Id out of bounds: id: $chapterId, chapters.size: ${chapters.size}")
}
val chapterStart = chapters[chapterId].chapterStartInMs
val chapterEnd =
if (chapterId + 1 < chapters.size) chapters[chapterId + 1].chapterStartInMs
else Long.MAX_VALUE
return playbackPosition in chapterStart..<chapterEnd
}
@Composable @Composable
fun ChapterItem( fun ChapterItem(
@ -62,41 +80,56 @@ fun ChapterItem(
thumbnail: Thumbnail?, thumbnail: Thumbnail?,
chapterTitle: String, chapterTitle: String,
chapterStartInMs: Long, chapterStartInMs: Long,
onClicked: (Int) -> Unit onClicked: (Int) -> Unit,
isCurrentChapter: Boolean
) { ) {
val locale = getLocale()!! val locale = getLocale()!!
Row( Box(
modifier = modifier modifier = modifier
.height(80.dp) .height(80.dp)
.clip(ITEM_CORNER_SHAPE) .clip(ITEM_CORNER_SHAPE)
.clickable { onClicked(id) } .clickable { onClicked(id) }
) { ) {
val contentDescription = stringResource(R.string.chapter_thumbnail) AnimatedVisibility(
Thumbnail( isCurrentChapter,
thumbnail = thumbnail, enter = fadeIn(animationSpec = tween(200)),
contentDescription = contentDescription, exit = fadeOut(animationSpec = tween(400))
shape = ITEM_CORNER_SHAPE
)
Column(
modifier = Modifier
.padding(start = 8.dp, top = 5.dp, bottom = 5.dp)
.weight(1f),
horizontalAlignment = Alignment.Start,
) { ) {
Text( Surface(
text = chapterTitle, modifier = Modifier.fillMaxSize(),
fontSize = 18.sp, color = Color.White.copy(alpha = 0.2f),
fontWeight = FontWeight.Bold, ) {}
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
getTimeStringFromMs(chapterStartInMs, locale),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
} }
Row {
val contentDescription = stringResource(R.string.chapter_thumbnail)
Thumbnail(
thumbnail = thumbnail,
contentDescription = contentDescription,
shape = ITEM_CORNER_SHAPE
)
Column(
modifier = Modifier
.padding(start = 8.dp, top = 5.dp, bottom = 5.dp)
.weight(1f),
horizontalAlignment = Alignment.Start,
) {
Text(
text = chapterTitle,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
getTimeStringFromMs(chapterStartInMs, locale),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
} }
} }
@ -111,7 +144,8 @@ fun ChapterItemPreview() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
chapterTitle = "Chapter Title", chapterTitle = "Chapter Title",
chapterStartInMs = (4 * 60 + 32) * 1000, chapterStartInMs = (4 * 60 + 32) * 1000,
onClicked = {} onClicked = {},
isCurrentChapter = false
) )
} }
} }

View File

@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.PlaylistAdd
import androidx.compose.material.icons.filled.Repeat import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.RepeatOn import androidx.compose.material.icons.filled.RepeatOn
import androidx.compose.material.icons.filled.RepeatOneOn import androidx.compose.material.icons.filled.RepeatOneOn
@ -43,8 +42,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.media3.common.Player
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.RepeatMode import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.model.VideoPlayerUIState import net.newpipe.newplayer.model.VideoPlayerUIState
@ -79,7 +76,7 @@ fun StreamSelectTopBar(
) )
}, actions = { }, actions = {
IconButton( IconButton(
onClick = viewModel::cycleRepeatmode onClick = viewModel::cycleRepeatMode
) { ) {
when (uiState.repeatMode) { when (uiState.repeatMode) {
RepeatMode.DONT_REPEAT -> Icon( RepeatMode.DONT_REPEAT -> Icon(