add chapter select preview

This commit is contained in:
Christian Schabesberger 2024-08-30 12:13:10 +02:00
parent cb122306c6
commit 22d7bcf552
6 changed files with 109 additions and 50 deletions

View File

@ -24,7 +24,7 @@ import android.net.Uri
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail
data class Chapter(val chapterStartInMs: Long, val chapterTitle: String?) data class Chapter(val chapterStartInMs: Long, val chapterTitle: String?, val thumbnail: Thumbnail?)
data class MetaInfo( data class MetaInfo(
val title: String, val title: String,
@ -46,7 +46,6 @@ interface MediaRepository {
suspend fun getPreviewThumbnails(item: String): HashMap<Long, Thumbnail>? suspend fun getPreviewThumbnails(item: String): HashMap<Long, Thumbnail>?
suspend fun getChapters(item: String): List<Chapter> suspend fun getChapters(item: String): List<Chapter>
suspend fun getChapterThumbnail(item: String, chapter: Long): Thumbnail?
suspend fun getTimestampLink(item: String, timestampInSeconds: Long): String suspend fun getTimestampLink(item: String, timestampInSeconds: Long): String

View File

@ -24,6 +24,7 @@ import android.os.Bundle
import androidx.media3.common.Player 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.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail
@ -58,4 +59,6 @@ interface VideoPlayerViewModel {
fun volumeChange(changeRate: Float) fun volumeChange(changeRate: Float)
fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig) fun openStreamSelection(selectChapter: Boolean, embeddedUiConfig: EmbeddedUiConfig)
fun closeStreamSelection() fun closeStreamSelection()
fun chapterSelected(chapter: Chapter)
fun streamSelected(streamId: Int)
} }

View File

@ -43,6 +43,7 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.utils.VideoSize import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromItemList import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromItemList
@ -399,6 +400,14 @@ class VideoPlayerViewModelImpl @Inject constructor(
updateUiMode(UIModeState.FULLSCREEN_VIDEO) updateUiMode(UIModeState.FULLSCREEN_VIDEO)
} }
override fun chapterSelected(chapter: Chapter) {
println("gurken chapter selectd: $chapter")
}
override fun streamSelected(streamId: Int) {
println("stream selected: $streamId")
}
private fun updateUiMode(newState: UIModeState) { private fun updateUiMode(newState: UIModeState) {
val newPlayMode = newState.toPlayMode() val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode() val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()

View File

@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
@ -83,6 +84,14 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
println("dummy impl") println("dummy impl")
} }
override fun chapterSelected(chapter: Chapter) {
println("dummp impl chapter selected: $chapter")
}
override fun streamSelected(streamId: Int) {
println("dummy impl stream selected: $streamId")
}
override fun pause() { override fun pause() {
println("dummy pause") println("dummy pause")
} }

View File

@ -21,29 +21,32 @@
package net.newpipe.newplayer.ui.videoplayer package net.newpipe.newplayer.ui.videoplayer
import android.view.MotionEvent import android.view.MotionEvent
import android.view.Surface
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.Arrangement
import androidx.compose.foundation.layout.Box 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.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
@ -65,6 +68,7 @@ import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
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 coil.compose.AsyncImage import coil.compose.AsyncImage
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.utils.BitmapThumbnail import net.newpipe.newplayer.utils.BitmapThumbnail
import net.newpipe.newplayer.utils.OnlineThumbnail import net.newpipe.newplayer.utils.OnlineThumbnail
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail
@ -79,12 +83,14 @@ fun StreamSelectUI(
) { ) {
val insets = getInsets() val insets = getInsets()
Surface( Surface(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize()
.windowInsetsPadding(insets),
color = CONTROLLER_UI_BACKGROUND_COLOR color = CONTROLLER_UI_BACKGROUND_COLOR
) { ) {
Scaffold( Scaffold(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(insets),
containerColor = Color.Transparent,
topBar = { topBar = {
if (isChapterSelect) { if (isChapterSelect) {
ChapterSelectTopBar(onClose = { ChapterSelectTopBar(onClose = {
@ -95,37 +101,50 @@ fun StreamSelectUI(
} }
} }
) { innerPadding -> ) { innerPadding ->
Surface( LazyColumn(
modifier = Modifier modifier = Modifier
.padding(innerPadding) .padding(innerPadding)
.fillMaxSize(), color = Color.Red .fillMaxSize(),
) { ) {
if (isChapterSelect) {
items(uiState.chapters.size) { chapterIndex ->
val chapter = uiState.chapters[chapterIndex]
ChapterItem(
id = chapterIndex,
chapterTitle = chapter.chapterTitle ?: "",
chapterStartInMs = chapter.chapterStartInMs,
thumbnail = chapter.thumbnail,
onClicked = {
viewModel.chapterSelected(chapter)
}
)
}
} else {
}
} }
} }
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ChapterSelectTopBar(modifier: Modifier = Modifier, onClose: () -> Unit) { private fun ChapterSelectTopBar(modifier: Modifier = Modifier, onClose: () -> Unit) {
Row( TopAppBar(modifier = modifier,
modifier = modifier, colors = topAppBarColors(containerColor = Color.Transparent),
verticalAlignment = Alignment.CenterVertically, title = {
horizontalArrangement = Arrangement.SpaceBetween Text("Chapter TODO")
) { //Text(stringResource(R.string.chapter))
Spacer(modifier = Modifier.width(20.dp)) }, actions = {
//Text(stringResource(R.string.chapter)) IconButton(
Text("Chapter TODO") onClick = onClose
Spacer(modifier = Modifier.weight(1F)) ) {
IconButton( Icon(
onClick = onClose imageVector = Icons.Filled.Close,
) { contentDescription = stringResource(R.string.close_chapter_selection)
Icon( )
imageVector = Icons.Filled.Close, }
contentDescription = stringResource(R.string.close_chapter_selection) })
)
}
}
} }
@Composable @Composable
@ -150,7 +169,7 @@ private fun ChapterItem(
top = 4.dp, top = 4.dp,
bottom = 4.dp, bottom = 4.dp,
end = 4.dp end = 4.dp
) ).height(80.dp)
.clickable { onClicked(id) } .clickable { onClicked(id) }
) { ) {
val contentDescription = stringResource(R.string.chapter) val contentDescription = stringResource(R.string.chapter)
@ -348,11 +367,29 @@ fun ChapterTopBarPreview() {
@Composable @Composable
fun VideoPlayerStreamSelectUIPreview() { fun VideoPlayerStreamSelectUIPreview() {
VideoPlayerTheme { VideoPlayerTheme {
Surface(modifier = Modifier.fillMaxSize(), color = Color.Green) { Surface(modifier = Modifier.fillMaxSize(), color = Color.Red) {
StreamSelectUI( StreamSelectUI(
isChapterSelect = true, isChapterSelect = true,
viewModel = VideoPlayerViewModelDummy(), viewModel = VideoPlayerViewModelDummy(),
uiState = VideoPlayerUIState.DEFAULT uiState = VideoPlayerUIState.DEFAULT.copy(
chapters = arrayListOf(
Chapter(
chapterStartInMs = 5000,
chapterTitle = "First Chapter",
thumbnail = null
),
Chapter(
chapterStartInMs = 10000,
chapterTitle = "Second Chapter",
thumbnail = null
),
Chapter(
chapterStartInMs = 20000,
chapterTitle = "Third Chapter",
thumbnail = null
),
)
)
) )
} }
} }

View File

@ -30,12 +30,14 @@ class TestMediaRepository(val context: Context) : MediaRepository {
thumbnail = OnlineThumbnail(context.getString(R.string.ccc_6502_thumbnail)), thumbnail = OnlineThumbnail(context.getString(R.string.ccc_6502_thumbnail)),
lengthInS = context.resources.getInteger(R.integer.ccc_6502_length) lengthInS = context.resources.getInteger(R.integer.ccc_6502_length)
) )
"imu" -> MetaInfo( "imu" -> MetaInfo(
title = context.getString(R.string.ccc_imu_title), title = context.getString(R.string.ccc_imu_title),
channelName = context.getString(R.string.ccc_imu_channel), channelName = context.getString(R.string.ccc_imu_channel),
thumbnail = OnlineThumbnail(context.getString(R.string.ccc_imu_thumbnail)), thumbnail = OnlineThumbnail(context.getString(R.string.ccc_imu_thumbnail)),
lengthInS = context.resources.getInteger(R.integer.ccc_imu_length) lengthInS = context.resources.getInteger(R.integer.ccc_imu_length)
) )
"portrait" -> MetaInfo( "portrait" -> MetaInfo(
title = context.getString(R.string.portrait_title), title = context.getString(R.string.portrait_title),
channelName = context.getString(R.string.portrait_channel), channelName = context.getString(R.string.portrait_channel),
@ -113,29 +115,29 @@ class TestMediaRepository(val context: Context) : MediaRepository {
override suspend fun getChapters(item: String) = override suspend fun getChapters(item: String) =
when (item) { when (item) {
"6502" -> context.resources.getIntArray(R.array.ccc_6502_chapters) "6502" -> context.resources.getIntArray(R.array.ccc_6502_chapters)
"imu" -> TODO() "imu" -> context.resources.getIntArray(R.array.ccc_imu_chapters)
else -> intArrayOf() else -> intArrayOf()
}.map { }.map {
Chapter(it.toLong(), "Dummy Chapter at timestamp $it") Chapter(
} it.toLong(), chapterTitle = "Dummy Chapter at timestamp $it",
thumbnail = when (item) {
"6502" -> OnlineThumbnail(
String.format(
context.getString(R.string.ccc_6502_preview_thumbnails),
it / (10 * 1000)
)
)
override suspend fun getChapterThumbnail(item: String, chapter: Long) = "imu" -> OnlineThumbnail(
when (item) { String.format(
"6502" -> OnlineThumbnail( context.getString(R.string.ccc_imu_preview_thumbnails),
String.format( it / (10 * 1000)
context.getString(R.string.ccc_6502_preview_thumbnails), )
chapter / (10 * 1000) )
)
else -> null
}
) )
"imu" -> OnlineThumbnail(
String.format(
context.getString(R.string.ccc_imu_preview_thumbnails),
chapter / (10 * 1000)
)
)
else -> null
} }
override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) = override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) =