From bf1c9ba7b52fe9f56602640337f6842c10bba0d1 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 1 Jul 2024 07:33:49 +0530 Subject: [PATCH] Start implementing stream composable, grid layout --- .../newpipe/compose/playlist/Playlist.kt | 26 ++-- .../compose/playlist/PlaylistHeader.kt | 22 +-- .../newpipe/compose/stream/StreamGridItem.kt | 130 ++++++++++++++++++ 3 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/compose/stream/StreamGridItem.kt diff --git a/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt b/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt index 941c2ce5a..3d73d55a2 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt @@ -1,11 +1,12 @@ package org.schabi.newpipe.compose.playlist import android.content.res.Configuration -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.HorizontalDivider +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -14,7 +15,9 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.paging.compose.collectAsLazyPagingItems +import my.nanihadesuka.compose.LazyVerticalGridScrollbar import org.schabi.newpipe.DownloaderImpl +import org.schabi.newpipe.compose.stream.StreamGridItem import org.schabi.newpipe.compose.theme.AppTheme import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.ServiceList @@ -30,14 +33,17 @@ fun Playlist(playlistViewModel: PlaylistViewModel = viewModel()) { playlistInfo?.let { Surface(color = MaterialTheme.colorScheme.background) { - LazyColumn { - item { - PlaylistHeader(playlistInfo = it, totalDuration = totalDuration) - HorizontalDivider(thickness = 1.dp) - } + val gridState = rememberLazyGridState() - items(streams.itemCount) { - Text(text = streams[it]!!.name) + LazyVerticalGridScrollbar(state = gridState) { + LazyVerticalGrid(state = gridState, columns = GridCells.Adaptive(164.dp)) { + item(span = { GridItemSpan(maxLineSpan) }) { + PlaylistHeader(playlistInfo = it, totalDuration = totalDuration) + } + + items(streams.itemCount) { + StreamGridItem(streams[it]!!) + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt index 882809342..9130ef5a8 100644 --- a/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt @@ -35,16 +35,12 @@ import org.schabi.newpipe.R import org.schabi.newpipe.compose.common.DescriptionText import org.schabi.newpipe.compose.theme.AppTheme import org.schabi.newpipe.error.ErrorUtil -import org.schabi.newpipe.extractor.Image import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.ServiceList import org.schabi.newpipe.extractor.playlist.PlaylistInfo import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper import org.schabi.newpipe.extractor.stream.Description -import org.schabi.newpipe.extractor.stream.StreamInfoItem -import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.util.Localization -import org.schabi.newpipe.util.NO_SERVICE_ID import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.image.ImageStrategy import java.util.concurrent.TimeUnit @@ -54,7 +50,7 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) { val context = LocalContext.current Column( - modifier = Modifier.padding(4.dp), + modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( @@ -148,22 +144,6 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) { } } -fun StreamInfoItem( - serviceId: Int = NO_SERVICE_ID, - url: String, - name: String, - streamType: StreamType = StreamType.NONE, - uploaderName: String? = null, - uploaderUrl: String? = null, - uploaderAvatars: List = emptyList(), - duration: Long, -) = StreamInfoItem(serviceId, url, name, streamType).apply { - this.uploaderName = uploaderName - this.uploaderUrl = uploaderUrl - this.uploaderAvatars = uploaderAvatars - this.duration = duration -} - @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/app/src/main/java/org/schabi/newpipe/compose/stream/StreamGridItem.kt b/app/src/main/java/org/schabi/newpipe/compose/stream/StreamGridItem.kt new file mode 100644 index 000000000..696300a35 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/compose/stream/StreamGridItem.kt @@ -0,0 +1,130 @@ +package org.schabi.newpipe.compose.stream + +import android.content.Context +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentActivity +import coil.compose.AsyncImage +import org.schabi.newpipe.R +import org.schabi.newpipe.compose.theme.AppTheme +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.util.NavigationHelper +import org.schabi.newpipe.util.image.ImageStrategy +import java.util.concurrent.TimeUnit + +@Composable +fun StreamGridItem(stream: StreamInfoItem) { + val context = LocalContext.current + + Column( + modifier = Modifier + .clickable { + NavigationHelper.openVideoDetailFragment( + context, (context as FragmentActivity).supportFragmentManager, + stream.serviceId, stream.url, stream.name, null, false + ) + } + .padding(12.dp) + ) { + AsyncImage( + model = ImageStrategy.choosePreferredImage(stream.thumbnails), + contentDescription = null, + placeholder = painterResource(R.drawable.placeholder_thumbnail_video), + error = painterResource(R.drawable.placeholder_thumbnail_video), + modifier = Modifier.size(width = 164.dp, height = 92.dp) + ) + + Text( + text = stream.name, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + maxLines = 2 + ) + + Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall) + + Text(text = getStreamInfoDetail(context, stream), style = MaterialTheme.typography.bodySmall) + } +} + +private fun getStreamInfoDetail(context: Context, stream: StreamInfoItem): String { + val views = if (stream.viewCount >= 0) { + when (stream.streamType) { + StreamType.AUDIO_LIVE_STREAM -> Localization.listeningCount(context, stream.viewCount) + StreamType.LIVE_STREAM -> Localization.shortWatchingCount(context, stream.viewCount) + else -> Localization.shortViewCount(context, stream.viewCount) + } + } else { + "" + } + val date = + Localization.relativeTimeOrTextual(context, stream.uploadDate, stream.textualUploadDate) + + return if (views.isEmpty()) { + date + } else if (date.isNullOrEmpty()) { + views + } else { + "$views • $date" + } +} + +fun StreamInfoItem( + serviceId: Int = NO_SERVICE_ID, + url: String = "", + name: String = "Stream", + streamType: StreamType, + uploaderName: String? = "Uploader", + uploaderUrl: String? = null, + uploaderAvatars: List = emptyList(), + duration: Long = TimeUnit.HOURS.toSeconds(1), + viewCount: Long = 10, + textualUploadDate: String = "1 month ago" +) = StreamInfoItem(serviceId, url, name, streamType).apply { + this.uploaderName = uploaderName + this.uploaderUrl = uploaderUrl + this.uploaderAvatars = uploaderAvatars + this.duration = duration + this.viewCount = viewCount + this.textualUploadDate = textualUploadDate +} + +private class StreamItemPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + StreamInfoItem(streamType = StreamType.NONE), + StreamInfoItem(streamType = StreamType.LIVE_STREAM), + StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM), + ) +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StreamGridItemPreview( + @PreviewParameter(StreamItemPreviewProvider::class) stream: StreamInfoItem +) { + AppTheme { + Surface(color = MaterialTheme.colorScheme.background) { + StreamGridItem(stream) + } + } +}