mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-01-10 09:20:31 +00:00
Implement card and list layouts, check for preferred layout from settings
This commit is contained in:
parent
bf1c9ba7b5
commit
72bbe0ea1c
@ -288,6 +288,7 @@ dependencies {
|
|||||||
// Jetpack Compose
|
// Jetpack Compose
|
||||||
implementation(platform('androidx.compose:compose-bom:2024.06.00'))
|
implementation(platform('androidx.compose:compose-bom:2024.06.00'))
|
||||||
implementation 'androidx.compose.material3:material3:1.3.0-beta05'
|
implementation 'androidx.compose.material3:material3:1.3.0-beta05'
|
||||||
|
implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0-beta04'
|
||||||
implementation 'androidx.activity:activity-compose'
|
implementation 'androidx.activity:activity-compose'
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
implementation 'androidx.compose.ui:ui-text:1.7.0-beta06' // Needed for parsing HTML to AnnotatedString
|
implementation 'androidx.compose.ui:ui-text:1.7.0-beta06' // Needed for parsing HTML to AnnotatedString
|
||||||
|
@ -1,28 +1,39 @@
|
|||||||
package org.schabi.newpipe.compose.playlist
|
package org.schabi.newpipe.compose.playlist
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import my.nanihadesuka.compose.LazyColumnScrollbar
|
||||||
import my.nanihadesuka.compose.LazyVerticalGridScrollbar
|
import my.nanihadesuka.compose.LazyVerticalGridScrollbar
|
||||||
import org.schabi.newpipe.DownloaderImpl
|
import org.schabi.newpipe.DownloaderImpl
|
||||||
|
import org.schabi.newpipe.compose.stream.StreamCardItem
|
||||||
import org.schabi.newpipe.compose.stream.StreamGridItem
|
import org.schabi.newpipe.compose.stream.StreamGridItem
|
||||||
|
import org.schabi.newpipe.compose.stream.StreamListItem
|
||||||
import org.schabi.newpipe.compose.theme.AppTheme
|
import org.schabi.newpipe.compose.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.compose.util.determineItemViewMode
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.info_list.ItemViewMode
|
||||||
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
||||||
import org.schabi.newpipe.util.KEY_URL
|
import org.schabi.newpipe.util.KEY_URL
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.viewmodels.PlaylistViewModel
|
import org.schabi.newpipe.viewmodels.PlaylistViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -31,18 +42,50 @@ fun Playlist(playlistViewModel: PlaylistViewModel = viewModel()) {
|
|||||||
val streams = playlistViewModel.streamItems.collectAsLazyPagingItems()
|
val streams = playlistViewModel.streamItems.collectAsLazyPagingItems()
|
||||||
val totalDuration = streams.itemSnapshotList.sumOf { it!!.duration }
|
val totalDuration = streams.itemSnapshotList.sumOf { it!!.duration }
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val onClick = { stream: StreamInfoItem ->
|
||||||
|
NavigationHelper.openVideoDetailFragment(
|
||||||
|
context, (context as FragmentActivity).supportFragmentManager,
|
||||||
|
stream.serviceId, stream.url, stream.name, null, false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
playlistInfo?.let {
|
playlistInfo?.let {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
val gridState = rememberLazyGridState()
|
val mode = determineItemViewMode()
|
||||||
|
|
||||||
LazyVerticalGridScrollbar(state = gridState) {
|
if (mode == ItemViewMode.GRID) {
|
||||||
LazyVerticalGrid(state = gridState, columns = GridCells.Adaptive(164.dp)) {
|
val gridState = rememberLazyGridState()
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
||||||
PlaylistHeader(playlistInfo = it, totalDuration = totalDuration)
|
LazyVerticalGridScrollbar(state = gridState) {
|
||||||
|
LazyVerticalGrid(state = gridState, columns = GridCells.Adaptive(250.dp)) {
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
PlaylistHeader(playlistInfo = it, totalDuration = totalDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(streams.itemCount) {
|
||||||
|
StreamGridItem(streams[it]!!, onClick)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Card or list views
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
items(streams.itemCount) {
|
LazyColumnScrollbar(state = listState) {
|
||||||
StreamGridItem(streams[it]!!)
|
LazyColumn(state = listState) {
|
||||||
|
item {
|
||||||
|
PlaylistHeader(playlistInfo = it, totalDuration = totalDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(streams.itemCount) {
|
||||||
|
val stream = streams[it]!!
|
||||||
|
if (mode == ItemViewMode.CARD) {
|
||||||
|
StreamCardItem(stream, onClick)
|
||||||
|
} else {
|
||||||
|
StreamListItem(stream, onClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package org.schabi.newpipe.compose.playlist
|
package org.schabi.newpipe.compose.playlist
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -22,6 +24,7 @@ import androidx.compose.runtime.setValue
|
|||||||
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.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -65,9 +68,9 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.apply {
|
modifier = Modifier.let {
|
||||||
if (playlistInfo.uploaderName != null && playlistInfo.uploaderUrl != null) {
|
if (playlistInfo.uploaderName != null && playlistInfo.uploaderUrl != null) {
|
||||||
clickable {
|
it.clickable {
|
||||||
try {
|
try {
|
||||||
NavigationHelper.openChannelFragment(
|
NavigationHelper.openChannelFragment(
|
||||||
(context as FragmentActivity).supportFragmentManager,
|
(context as FragmentActivity).supportFragmentManager,
|
||||||
@ -78,11 +81,13 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
|
|||||||
ErrorUtil.showUiErrorSnackbar(context, "Opening channel fragment", e)
|
ErrorUtil.showUiErrorSnackbar(context, "Opening channel fragment", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else it
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val imageModifier = Modifier
|
val imageModifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
|
.border(BorderStroke(1.dp, Color.White), CircleShape)
|
||||||
|
.padding(1.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
val isMix = YoutubeParsingHelper.isYoutubeMixId(playlistInfo.id) ||
|
val isMix = YoutubeParsingHelper.isYoutubeMixId(playlistInfo.id) ||
|
||||||
YoutubeParsingHelper.isYoutubeMusicMixId(playlistInfo.id)
|
YoutubeParsingHelper.isYoutubeMusicMixId(playlistInfo.id)
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
package org.schabi.newpipe.compose.stream
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.layout.ContentScale
|
||||||
|
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.unit.dp
|
||||||
|
import org.schabi.newpipe.compose.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StreamCardItem(stream: StreamInfoItem, onClick: (StreamInfoItem) -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onClick(stream) })
|
||||||
|
.padding(top = 12.dp, start = 2.dp, end = 2.dp)
|
||||||
|
) {
|
||||||
|
StreamThumbnail(
|
||||||
|
stream = stream,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentScale = ContentScale.FillWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(10.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stream.name,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
maxLines = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = getStreamInfoDetail(stream),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun StreamCardItemPreview(
|
||||||
|
@PreviewParameter(StreamItemPreviewProvider::class) stream: StreamInfoItem
|
||||||
|
) {
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
StreamCardItem(stream) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package org.schabi.newpipe.compose.stream
|
package org.schabi.newpipe.compose.stream
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -11,47 +10,21 @@ 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
|
||||||
import androidx.compose.ui.Modifier
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import androidx.compose.ui.unit.dp
|
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.compose.theme.AppTheme
|
||||||
import org.schabi.newpipe.extractor.Image
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
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
|
@Composable
|
||||||
fun StreamGridItem(stream: StreamInfoItem) {
|
fun StreamGridItem(stream: StreamInfoItem, onClick: (StreamInfoItem) -> Unit) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable(onClick = { onClick(stream) })
|
||||||
NavigationHelper.openVideoDetailFragment(
|
|
||||||
context, (context as FragmentActivity).supportFragmentManager,
|
|
||||||
stream.serviceId, stream.url, stream.name, null, false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
StreamThumbnail(stream = stream, modifier = Modifier.size(width = 246.dp, height = 138.dp))
|
||||||
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(
|
||||||
text = stream.name,
|
text = stream.name,
|
||||||
@ -62,60 +35,13 @@ fun StreamGridItem(stream: StreamInfoItem) {
|
|||||||
|
|
||||||
Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
|
Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
|
||||||
|
|
||||||
Text(text = getStreamInfoDetail(context, stream), style = MaterialTheme.typography.bodySmall)
|
Text(
|
||||||
|
text = getStreamInfoDetail(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<Image> = 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<StreamInfoItem> {
|
|
||||||
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 = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
@ -124,7 +50,7 @@ private fun StreamGridItemPreview(
|
|||||||
) {
|
) {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
StreamGridItem(stream)
|
StreamGridItem(stream, onClick = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
package org.schabi.newpipe.compose.stream
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.unit.dp
|
||||||
|
import org.schabi.newpipe.compose.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StreamListItem(stream: StreamInfoItem, onClick: (StreamInfoItem) -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onClick(stream) })
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
StreamThumbnail(stream = stream, modifier = Modifier.size(width = 98.dp, height = 55.dp))
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stream.name,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = getStreamInfoDetail(stream),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun StreamListItemPreview(
|
||||||
|
@PreviewParameter(StreamItemPreviewProvider::class) stream: StreamInfoItem
|
||||||
|
) {
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
StreamListItem(stream, onClick = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package org.schabi.newpipe.compose.stream
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StreamThumbnail(
|
||||||
|
stream: StreamInfoItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentScale: ContentScale = ContentScale.Fit,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageStrategy.choosePreferredImage(stream.thumbnails),
|
||||||
|
contentDescription = null,
|
||||||
|
placeholder = painterResource(R.drawable.placeholder_thumbnail_video),
|
||||||
|
error = painterResource(R.drawable.placeholder_thumbnail_video),
|
||||||
|
contentScale = contentScale,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = Localization.getDurationString(stream.duration),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(2.dp)
|
||||||
|
.background(Color.Black.copy(alpha = 0.5f))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package org.schabi.newpipe.compose.stream
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
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 java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
fun StreamInfoItem(
|
||||||
|
serviceId: Int = NO_SERVICE_ID,
|
||||||
|
url: String = "",
|
||||||
|
name: String = "Stream",
|
||||||
|
streamType: StreamType,
|
||||||
|
uploaderName: String? = "Uploader",
|
||||||
|
uploaderUrl: String? = null,
|
||||||
|
uploaderAvatars: List<Image> = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun getStreamInfoDetail(stream: StreamInfoItem): String {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
return rememberSaveable(stream) {
|
||||||
|
val count = stream.viewCount
|
||||||
|
val views = if (count >= 0) {
|
||||||
|
when (stream.streamType) {
|
||||||
|
StreamType.AUDIO_LIVE_STREAM -> Localization.listeningCount(context, count)
|
||||||
|
StreamType.LIVE_STREAM -> Localization.shortWatchingCount(context, count)
|
||||||
|
else -> Localization.shortViewCount(context, count)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
val date =
|
||||||
|
Localization.relativeTimeOrTextual(context, stream.uploadDate, stream.textualUploadDate)
|
||||||
|
|
||||||
|
if (views.isEmpty()) {
|
||||||
|
date
|
||||||
|
} else if (date.isNullOrEmpty()) {
|
||||||
|
views
|
||||||
|
} else {
|
||||||
|
"$views • $date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class StreamItemPreviewProvider : PreviewParameterProvider<StreamInfoItem> {
|
||||||
|
override val values = sequenceOf(
|
||||||
|
StreamInfoItem(streamType = StreamType.NONE),
|
||||||
|
StreamInfoItem(streamType = StreamType.LIVE_STREAM),
|
||||||
|
StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM),
|
||||||
|
)
|
||||||
|
}
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list.playlist
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
@ -15,13 +14,12 @@ class PlaylistFragment2 : Fragment() {
|
|||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View =
|
) = ComposeView(requireContext()).apply {
|
||||||
ComposeView(requireContext()).apply {
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
setContent {
|
||||||
setContent {
|
AppTheme {
|
||||||
AppTheme {
|
Playlist()
|
||||||
Playlist()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,6 @@ package org.schabi.newpipe.info_list;
|
|||||||
* Item view mode for streams & playlist listing screens.
|
* Item view mode for streams & playlist listing screens.
|
||||||
*/
|
*/
|
||||||
public enum ItemViewMode {
|
public enum ItemViewMode {
|
||||||
/**
|
|
||||||
* Default mode.
|
|
||||||
*/
|
|
||||||
AUTO,
|
|
||||||
/**
|
/**
|
||||||
* Full width list item with thumb on the left and two line title & uploader in right.
|
* Full width list item with thumb on the left and two line title & uploader in right.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package org.schabi.newpipe.compose.util
|
||||||
|
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.window.core.layout.WindowWidthSizeClass
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.info_list.ItemViewMode
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun determineItemViewMode(): ItemViewMode {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val listMode = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getString(
|
||||||
|
context.getString(R.string.list_view_mode_key),
|
||||||
|
context.getString(R.string.list_view_mode_value)
|
||||||
|
)
|
||||||
|
|
||||||
|
return when (listMode) {
|
||||||
|
context.getString(R.string.list_view_mode_list_key) -> ItemViewMode.LIST
|
||||||
|
context.getString(R.string.list_view_mode_grid_key) -> ItemViewMode.GRID
|
||||||
|
context.getString(R.string.list_view_mode_card_key) -> ItemViewMode.CARD
|
||||||
|
else -> {
|
||||||
|
// Auto mode - evaluate whether to use Grid based on screen real estate.
|
||||||
|
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||||
|
if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
|
||||||
|
ItemViewMode.GRID
|
||||||
|
} else {
|
||||||
|
ItemViewMode.LIST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user