diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt index 4632e06..9b30876 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt @@ -20,22 +20,19 @@ 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.PaddingValues -import androidx.compose.foundation.layout.Row 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.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color 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.rememberReorderableLazyListState +val ITEM_CORNER_SHAPE = RoundedCornerShape(10.dp) @Composable fun StreamSelectUI( @@ -85,34 +83,36 @@ fun StreamSelectUI( } } ) { innerPadding -> - if (isChapterSelect) { - LazyColumn( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - contentPadding = PaddingValues(start = 8.dp, end = 4.dp) - ) { + Box(modifier = Modifier.padding(innerPadding)) { + if (isChapterSelect) { + LazyColumn( + modifier = Modifier + .padding(start = 5.dp, end = 5.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 fun ReorderableStreamItemsList( - innerPadding: PaddingValues, + padding: PaddingValues, viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState ) { @@ -135,8 +135,9 @@ fun ReorderableStreamItemsList( LazyColumn( modifier = Modifier - .padding(innerPadding) + .padding(padding) .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(5.dp), state = lazyListState ) { itemsIndexed(uiState.playList, key = {_, item -> item.uniqueId}) { index, playlistItem -> @@ -145,16 +146,13 @@ fun ReorderableStreamItemsList( key = playlistItem.uniqueId ) { isDragging -> StreamItem( - uniqueId = playlistItem.uniqueId, - title = playlistItem.title, - creator = playlistItem.creator, - thumbnail = playlistItem.thumbnail, - lengthInMs = playlistItem.lengthInS.toLong() * 1000, + playlistItem = playlistItem, onClicked = { viewModel.streamSelected(0) }, reorderableScope = this@ReorderableItem, haptic = haptic, onDragFinished = viewModel::onStreamItemDragFinished, - isDragging = isDragging + isDragging = isDragging, + isCurrentlyPlaying = playlistItem.uniqueId == uiState.currentlyPlaying.uniqueId ) } } @@ -227,7 +225,8 @@ fun VideoPlayerStreamSelectUIPreview() { thumbnail = null, uniqueId = 2 ) - ) + ), + currentlyPlaying = PlaylistItem.DUMMY.copy(uniqueId = 1) ) ) } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterItem.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterItem.kt index 7b92bf2..29905b0 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterItem.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/ChapterItem.kt @@ -33,6 +33,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -44,6 +45,7 @@ import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import net.newpipe.newplayer.R 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.OnlineThumbnail import net.newpipe.newplayer.utils.Thumbnail @@ -65,39 +67,19 @@ fun ChapterItem( Row( modifier = modifier .height(80.dp) - .padding(5.dp) + .clip(ITEM_CORNER_SHAPE) .clickable { onClicked(id) } ) { val contentDescription = stringResource(R.string.chapter_thumbnail) - if (thumbnail != null) { - when (thumbnail) { - is OnlineThumbnail -> AsyncImage( - model = thumbnail.url, - 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) - ) - } + Thumbnail( + thumbnail = thumbnail, + contentDescription = contentDescription, + shape = ITEM_CORNER_SHAPE + ) Column( - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier + .padding(start = 8.dp, top = 5.dp, bottom = 5.dp) + .weight(1f), horizontalAlignment = Alignment.Start, ) { Text( diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt index 5a4e098..b2d6c8c 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt @@ -38,6 +38,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material3.Icon @@ -49,6 +50,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -59,8 +61,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage 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.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_OUT import net.newpipe.newplayer.utils.BitmapThumbnail @@ -73,56 +77,26 @@ import net.newpipe.newplayer.utils.getLocale import net.newpipe.newplayer.utils.getTimeStringFromMs 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 fun StreamItem( modifier: Modifier = Modifier, - uniqueId: Long, - title: String, - creator: String?, - thumbnail: Thumbnail?, - lengthInMs: Long, + playlistItem: PlaylistItem, onClicked: (Long) -> Unit, onDragFinished: () -> Unit, reorderableScope: ReorderableCollectionItemScope?, haptic: ReorderHapticFeedback?, - isDragging: Boolean + isDragging: Boolean, + isCurrentlyPlaying: Boolean ) { val locale = getLocale()!! val interactionSource = remember { MutableInteractionSource() } - Box(modifier = modifier.height(60.dp).clickable { - onClicked(uniqueId) - }) { + Box(modifier = modifier + .height(60.dp) + .clip(ITEM_CORNER_SHAPE) + .clickable { + onClicked(playlistItem.uniqueId) + }) { AnimatedVisibility( visible = isDragging, @@ -132,13 +106,25 @@ fun StreamItem( Surface( modifier = Modifier.fillMaxSize(), 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( modifier = modifier - .padding(5.dp) + .padding(0.dp) ) { Box( modifier = Modifier @@ -146,9 +132,14 @@ fun StreamItem( .fillMaxSize() ) { val contentDescription = stringResource(R.string.stream_item_thumbnail) - Thumbnail(thumbnail, contentDescription) + Thumbnail( + thumbnail = playlistItem.thumbnail, + contentDescription = contentDescription, + shape = ITEM_CORNER_SHAPE + ) Surface( color = CONTROLLER_UI_BACKGROUND_COLOR, + shape = ITEM_CORNER_SHAPE, modifier = Modifier .wrapContentSize() .align(Alignment.BottomEnd) @@ -162,7 +153,7 @@ fun StreamItem( bottom = 0.5.dp ), text = getTimeStringFromMs( - lengthInMs, + playlistItem.lengthInS * 1000L, locale, leadingZerosForMinutes = false ), @@ -173,24 +164,25 @@ fun StreamItem( Column( modifier = Modifier - .padding(1.dp) + .padding(6.dp) .weight(1f) .wrapContentHeight() .fillMaxWidth() ) { 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 ) - if (creator != null) { - Text( - text = creator, - fontSize = 13.sp, - fontWeight = FontWeight.Light, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } } IconButton( modifier = if (reorderableScope != null) { @@ -233,16 +225,13 @@ fun StreamItemPreview() { Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) { Box(modifier = Modifier.fillMaxSize()) { StreamItem( - uniqueId = 0, - title = "Video Title", - creator = "Video Creator", - thumbnail = null, - lengthInMs = 15 * 60 * 1000, + playlistItem = PlaylistItem.DUMMY, onClicked = {}, reorderableScope = null, haptic = null, onDragFinished = {}, isDragging = false, + isCurrentlyPlaying = true ) } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt b/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt index e3c739d..bd25589 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/utils/utils.kt @@ -24,9 +24,12 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.ContextWrapper +import android.graphics.drawable.shapes.Shape import android.view.WindowManager +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.waterfall @@ -34,11 +37,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect 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.LocalContext import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.core.os.ConfigurationCompat import androidx.core.view.WindowCompat +import coil.compose.AsyncImage +import net.newpipe.newplayer.R import net.newpipe.newplayer.model.EmbeddedUiConfig import java.util.Locale @@ -100,7 +108,7 @@ fun getEmbeddedUiConfig(activity: Activity): EmbeddedUiConfig { } @Composable -fun getInsets() = +fun getInsets() = WindowInsets.systemBars.union(WindowInsets.displayCutout).union(WindowInsets.waterfall) 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_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 millisThisDay = timeSpanInMs - days * MILLIS_PER_DAY val hours = millisThisDay / MILLIS_PER_HOUR @@ -126,7 +138,55 @@ fun getTimeStringFromMs(timeSpanInMs: Long, locale: Locale, leadingZerosForMinut val time_string = 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 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 } + +@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 + ) + } +} + +