add currently plaing indicator to playlist and make playlist use rounded corners

This commit is contained in:
Christian Schabesberger 2024-09-04 14:02:10 +02:00
parent a47ea8e078
commit 9f1c06928a
4 changed files with 160 additions and 130 deletions

View File

@ -20,22 +20,19 @@
package net.newpipe.newplayer.ui.videoplayer package net.newpipe.newplayer.ui.videoplayer
import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -57,6 +54,7 @@ import net.newpipe.newplayer.utils.rememberReorderHapticFeedback
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState import sh.calvin.reorderable.rememberReorderableLazyListState
val ITEM_CORNER_SHAPE = RoundedCornerShape(10.dp)
@Composable @Composable
fun StreamSelectUI( fun StreamSelectUI(
@ -85,34 +83,36 @@ fun StreamSelectUI(
} }
} }
) { innerPadding -> ) { innerPadding ->
if (isChapterSelect) { Box(modifier = Modifier.padding(innerPadding)) {
LazyColumn( if (isChapterSelect) {
modifier = Modifier LazyColumn(
.padding(innerPadding) modifier = Modifier
.fillMaxSize(), .padding(start = 5.dp, end = 5.dp)
contentPadding = PaddingValues(start = 8.dp, end = 4.dp) .fillMaxSize(),
) { verticalArrangement = Arrangement.spacedBy(5.dp),
) {
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)
}
)
}
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 {
ReorderableStreamItemsList(
padding = PaddingValues(start = 5.dp, end = 5.dp),
viewModel = viewModel,
uiState = uiState
)
} }
} else {
ReorderableStreamItemsList(
innerPadding = innerPadding,
viewModel = viewModel,
uiState = uiState
)
} }
} }
} }
@ -120,7 +120,7 @@ fun StreamSelectUI(
@Composable @Composable
fun ReorderableStreamItemsList( fun ReorderableStreamItemsList(
innerPadding: PaddingValues, padding: PaddingValues,
viewModel: VideoPlayerViewModel, viewModel: VideoPlayerViewModel,
uiState: VideoPlayerUIState uiState: VideoPlayerUIState
) { ) {
@ -135,8 +135,9 @@ fun ReorderableStreamItemsList(
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.padding(innerPadding) .padding(padding)
.fillMaxSize(), .fillMaxSize(),
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 ->
@ -145,16 +146,13 @@ fun ReorderableStreamItemsList(
key = playlistItem.uniqueId key = playlistItem.uniqueId
) { isDragging -> ) { isDragging ->
StreamItem( StreamItem(
uniqueId = playlistItem.uniqueId, playlistItem = playlistItem,
title = playlistItem.title,
creator = playlistItem.creator,
thumbnail = playlistItem.thumbnail,
lengthInMs = playlistItem.lengthInS.toLong() * 1000,
onClicked = { viewModel.streamSelected(0) }, onClicked = { viewModel.streamSelected(0) },
reorderableScope = this@ReorderableItem, reorderableScope = this@ReorderableItem,
haptic = haptic, haptic = haptic,
onDragFinished = viewModel::onStreamItemDragFinished, onDragFinished = viewModel::onStreamItemDragFinished,
isDragging = isDragging isDragging = isDragging,
isCurrentlyPlaying = playlistItem.uniqueId == uiState.currentlyPlaying.uniqueId
) )
} }
} }
@ -227,7 +225,8 @@ fun VideoPlayerStreamSelectUIPreview() {
thumbnail = null, thumbnail = null,
uniqueId = 2 uniqueId = 2
) )
) ),
currentlyPlaying = PlaylistItem.DUMMY.copy(uniqueId = 1)
) )
) )
} }

View File

@ -33,6 +33,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -44,6 +45,7 @@ import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
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.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
@ -65,39 +67,19 @@ fun ChapterItem(
Row( Row(
modifier = modifier modifier = modifier
.height(80.dp) .height(80.dp)
.padding(5.dp) .clip(ITEM_CORNER_SHAPE)
.clickable { onClicked(id) } .clickable { onClicked(id) }
) { ) {
val contentDescription = stringResource(R.string.chapter_thumbnail) val contentDescription = stringResource(R.string.chapter_thumbnail)
if (thumbnail != null) { Thumbnail(
when (thumbnail) { thumbnail = thumbnail,
is OnlineThumbnail -> AsyncImage( contentDescription = contentDescription,
model = thumbnail.url, shape = ITEM_CORNER_SHAPE
contentDescription = contentDescription )
)
is BitmapThumbnail -> Image(
bitmap = thumbnail.img,
contentDescription = contentDescription
)
is VectorThumbnail -> Image(
imageVector = thumbnail.vec,
contentDescription = contentDescription
)
}
AsyncImage(
model = thumbnail,
contentDescription = contentDescription
)
} else {
Image(
painterResource(R.drawable.tiny_placeholder),
contentDescription = stringResource(R.string.chapter_thumbnail)
)
}
Column( Column(
modifier = Modifier.padding(start = 8.dp), modifier = Modifier
.padding(start = 8.dp, top = 5.dp, bottom = 5.dp)
.weight(1f),
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
) { ) {
Text( Text(

View File

@ -38,6 +38,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -49,6 +50,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -59,8 +61,10 @@ 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.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR
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.gesture_ui.SEEK_ANIMATION_FADE_IN import net.newpipe.newplayer.ui.videoplayer.gesture_ui.SEEK_ANIMATION_FADE_IN
import net.newpipe.newplayer.ui.videoplayer.gesture_ui.SEEK_ANIMATION_FADE_OUT import net.newpipe.newplayer.ui.videoplayer.gesture_ui.SEEK_ANIMATION_FADE_OUT
import net.newpipe.newplayer.utils.BitmapThumbnail import net.newpipe.newplayer.utils.BitmapThumbnail
@ -73,56 +77,26 @@ import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs import net.newpipe.newplayer.utils.getTimeStringFromMs
import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableCollectionItemScope
@Composable
private fun Thumbnail(thumbnail: Thumbnail?, contentDescription: String) {
if (thumbnail != null) {
when (thumbnail) {
is OnlineThumbnail -> AsyncImage(
modifier = Modifier.fillMaxSize(),
model = thumbnail.url,
contentDescription = contentDescription
)
is BitmapThumbnail -> Image(
modifier = Modifier.fillMaxSize(),
bitmap = thumbnail.img,
contentDescription = contentDescription
)
is VectorThumbnail -> Image(
modifier = Modifier.fillMaxSize(),
imageVector = thumbnail.vec,
contentDescription = contentDescription
)
}
} else {
Image(
painter = painterResource(R.drawable.tiny_placeholder),
contentDescription = contentDescription
)
}
}
@Composable @Composable
fun StreamItem( fun StreamItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
uniqueId: Long, playlistItem: PlaylistItem,
title: String,
creator: String?,
thumbnail: Thumbnail?,
lengthInMs: Long,
onClicked: (Long) -> Unit, onClicked: (Long) -> Unit,
onDragFinished: () -> Unit, onDragFinished: () -> Unit,
reorderableScope: ReorderableCollectionItemScope?, reorderableScope: ReorderableCollectionItemScope?,
haptic: ReorderHapticFeedback?, haptic: ReorderHapticFeedback?,
isDragging: Boolean isDragging: Boolean,
isCurrentlyPlaying: Boolean
) { ) {
val locale = getLocale()!! val locale = getLocale()!!
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box(modifier = modifier.height(60.dp).clickable { Box(modifier = modifier
onClicked(uniqueId) .height(60.dp)
}) { .clip(ITEM_CORNER_SHAPE)
.clickable {
onClicked(playlistItem.uniqueId)
}) {
AnimatedVisibility( AnimatedVisibility(
visible = isDragging, visible = isDragging,
@ -132,13 +106,25 @@ fun StreamItem(
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background, color = MaterialTheme.colorScheme.background,
shadowElevation = 8.dp shadowElevation = 8.dp,
shape = ITEM_CORNER_SHAPE
) {}
}
AnimatedVisibility(
visible = isCurrentlyPlaying,
enter = fadeIn(animationSpec = tween(200)),
exit = fadeOut(animationSpec = tween(400))
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.White.copy(alpha = 0.2f),
) {} ) {}
} }
Row( Row(
modifier = modifier modifier = modifier
.padding(5.dp) .padding(0.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@ -146,9 +132,14 @@ fun StreamItem(
.fillMaxSize() .fillMaxSize()
) { ) {
val contentDescription = stringResource(R.string.stream_item_thumbnail) val contentDescription = stringResource(R.string.stream_item_thumbnail)
Thumbnail(thumbnail, contentDescription) Thumbnail(
thumbnail = playlistItem.thumbnail,
contentDescription = contentDescription,
shape = ITEM_CORNER_SHAPE
)
Surface( Surface(
color = CONTROLLER_UI_BACKGROUND_COLOR, color = CONTROLLER_UI_BACKGROUND_COLOR,
shape = ITEM_CORNER_SHAPE,
modifier = Modifier modifier = Modifier
.wrapContentSize() .wrapContentSize()
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
@ -162,7 +153,7 @@ fun StreamItem(
bottom = 0.5.dp bottom = 0.5.dp
), ),
text = getTimeStringFromMs( text = getTimeStringFromMs(
lengthInMs, playlistItem.lengthInS * 1000L,
locale, locale,
leadingZerosForMinutes = false leadingZerosForMinutes = false
), ),
@ -173,24 +164,25 @@ fun StreamItem(
Column( Column(
modifier = Modifier modifier = Modifier
.padding(1.dp) .padding(6.dp)
.weight(1f) .weight(1f)
.wrapContentHeight() .wrapContentHeight()
.fillMaxWidth() .fillMaxWidth()
) { ) {
Text( Text(
text = title, fontSize = 14.sp, fontWeight = FontWeight.Bold, maxLines = 1, text = playlistItem.title,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = playlistItem.creator,
fontSize = 13.sp,
fontWeight = FontWeight.Light,
maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
if (creator != null) {
Text(
text = creator,
fontSize = 13.sp,
fontWeight = FontWeight.Light,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
} }
IconButton( IconButton(
modifier = if (reorderableScope != null) { modifier = if (reorderableScope != null) {
@ -233,16 +225,13 @@ fun StreamItemPreview() {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) { Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
StreamItem( StreamItem(
uniqueId = 0, playlistItem = PlaylistItem.DUMMY,
title = "Video Title",
creator = "Video Creator",
thumbnail = null,
lengthInMs = 15 * 60 * 1000,
onClicked = {}, onClicked = {},
reorderableScope = null, reorderableScope = null,
haptic = null, haptic = null,
onDragFinished = {}, onDragFinished = {},
isDragging = false, isDragging = false,
isCurrentlyPlaying = true
) )
} }
} }

View File

@ -24,9 +24,12 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.graphics.drawable.shapes.Shape
import android.view.WindowManager import android.view.WindowManager
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.waterfall import androidx.compose.foundation.layout.waterfall
@ -34,11 +37,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import coil.compose.AsyncImage
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.EmbeddedUiConfig import net.newpipe.newplayer.model.EmbeddedUiConfig
import java.util.Locale import java.util.Locale
@ -100,7 +108,7 @@ fun getEmbeddedUiConfig(activity: Activity): EmbeddedUiConfig {
} }
@Composable @Composable
fun getInsets() = fun getInsets() =
WindowInsets.systemBars.union(WindowInsets.displayCutout).union(WindowInsets.waterfall) WindowInsets.systemBars.union(WindowInsets.displayCutout).union(WindowInsets.waterfall)
private const val HOURS_PER_DAY = 24 private const val HOURS_PER_DAY = 24
@ -113,7 +121,11 @@ private const val MILLIS_PER_DAY =
private const val MILLIS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLIS_PER_SECOND private const val MILLIS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLIS_PER_SECOND
private const val MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND private const val MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND
fun getTimeStringFromMs(timeSpanInMs: Long, locale: Locale, leadingZerosForMinutes:Boolean = true): String { fun getTimeStringFromMs(
timeSpanInMs: Long,
locale: Locale,
leadingZerosForMinutes: Boolean = true
): String {
val days = timeSpanInMs / MILLIS_PER_DAY val days = timeSpanInMs / MILLIS_PER_DAY
val millisThisDay = timeSpanInMs - days * MILLIS_PER_DAY val millisThisDay = timeSpanInMs - days * MILLIS_PER_DAY
val hours = millisThisDay / MILLIS_PER_HOUR val hours = millisThisDay / MILLIS_PER_HOUR
@ -126,7 +138,55 @@ fun getTimeStringFromMs(timeSpanInMs: Long, locale: Locale, leadingZerosForMinut
val time_string = val time_string =
if (0L < days) String.format(locale, "%d:%02d:%02d:%02d", days, hours, minutes, seconds) if (0L < days) String.format(locale, "%d:%02d:%02d:%02d", days, hours, minutes, seconds)
else if (0L < hours) String.format(locale, "%d:%02d:%02d", hours, minutes, seconds) else if (0L < hours) String.format(locale, "%d:%02d:%02d", hours, minutes, seconds)
else String.format(locale, if(leadingZerosForMinutes) "%02d:%02d" else "%d:%02d", minutes, seconds) else String.format(
locale,
if (leadingZerosForMinutes) "%02d:%02d" else "%d:%02d",
minutes,
seconds
)
return time_string return time_string
} }
@Composable
fun Thumbnail(
modifier: Modifier = Modifier,
thumbnail: Thumbnail?,
contentDescription: String,
shape: androidx.compose.ui.graphics.Shape? = null
) {
val modifier = if (shape == null) {
modifier
} else {
modifier
.clip(shape)
}
when (thumbnail) {
is OnlineThumbnail -> AsyncImage(
modifier = modifier,
model = thumbnail.url,
contentDescription = contentDescription
)
is BitmapThumbnail -> Image(
modifier = modifier,
bitmap = thumbnail.img,
contentDescription = contentDescription
)
is VectorThumbnail -> Image(
modifier = modifier,
imageVector = thumbnail.vec,
contentDescription = contentDescription
)
null -> Image(
modifier = modifier,
painter = painterResource(R.drawable.tiny_placeholder),
contentDescription = contentDescription
)
}
}