highlight active chapter
This commit is contained in:
parent
48f5e159d9
commit
79f8719ac3
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue