Skip to content

Commit d1adabc

Browse files
Implement card and list layouts, check for preferred layout from settings
1 parent d98a326 commit d1adabc

File tree

12 files changed

+375
-128
lines changed

12 files changed

+375
-128
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ dependencies {
291291
// Jetpack Compose
292292
implementation(platform('androidx.compose:compose-bom:2024.06.00'))
293293
implementation 'androidx.compose.material3:material3:1.3.0-beta04'
294+
implementation "androidx.compose.material3.adaptive:adaptive:1.0.0-beta04"
294295
implementation 'androidx.activity:activity-compose'
295296
implementation 'androidx.compose.ui:ui-tooling-preview'
296297
implementation 'androidx.compose.ui:ui-text:1.7.0-beta04' // Needed for parsing HTML to AnnotatedString

app/src/main/java/org/schabi/newpipe/compose/playlist/Playlist.kt

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
package org.schabi.newpipe.compose.playlist
22

33
import android.content.res.Configuration
4+
import androidx.compose.foundation.lazy.LazyColumn
45
import androidx.compose.foundation.lazy.grid.GridCells
56
import androidx.compose.foundation.lazy.grid.GridItemSpan
67
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
78
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
9+
import androidx.compose.foundation.lazy.rememberLazyListState
810
import androidx.compose.material3.MaterialTheme
911
import androidx.compose.material3.Surface
1012
import androidx.compose.runtime.Composable
1113
import androidx.compose.runtime.collectAsState
1214
import androidx.compose.runtime.getValue
15+
import androidx.compose.ui.platform.LocalContext
1316
import androidx.compose.ui.tooling.preview.Preview
1417
import androidx.compose.ui.unit.dp
18+
import androidx.fragment.app.FragmentActivity
1519
import androidx.lifecycle.SavedStateHandle
1620
import androidx.lifecycle.viewmodel.compose.viewModel
1721
import androidx.paging.compose.collectAsLazyPagingItems
22+
import my.nanihadesuka.compose.LazyColumnScrollbar
1823
import my.nanihadesuka.compose.LazyVerticalGridScrollbar
1924
import org.schabi.newpipe.DownloaderImpl
25+
import org.schabi.newpipe.compose.stream.StreamCardItem
2026
import org.schabi.newpipe.compose.stream.StreamGridItem
27+
import org.schabi.newpipe.compose.stream.StreamListItem
2128
import org.schabi.newpipe.compose.theme.AppTheme
29+
import org.schabi.newpipe.compose.util.determineItemViewMode
2230
import org.schabi.newpipe.extractor.NewPipe
2331
import org.schabi.newpipe.extractor.ServiceList
32+
import org.schabi.newpipe.extractor.stream.StreamInfoItem
33+
import org.schabi.newpipe.info_list.ItemViewMode
2434
import org.schabi.newpipe.util.KEY_SERVICE_ID
2535
import org.schabi.newpipe.util.KEY_URL
36+
import org.schabi.newpipe.util.NavigationHelper
2637
import org.schabi.newpipe.viewmodels.PlaylistViewModel
2738

2839
@Composable
@@ -31,18 +42,50 @@ fun Playlist(playlistViewModel: PlaylistViewModel = viewModel()) {
3142
val streams = playlistViewModel.streamItems.collectAsLazyPagingItems()
3243
val totalDuration = streams.itemSnapshotList.sumOf { it!!.duration }
3344

45+
val context = LocalContext.current
46+
val onClick = { stream: StreamInfoItem ->
47+
NavigationHelper.openVideoDetailFragment(
48+
context, (context as FragmentActivity).supportFragmentManager,
49+
stream.serviceId, stream.url, stream.name, null, false
50+
)
51+
}
52+
3453
playlistInfo?.let {
3554
Surface(color = MaterialTheme.colorScheme.background) {
36-
val gridState = rememberLazyGridState()
55+
val mode = determineItemViewMode()
56+
57+
if (mode == ItemViewMode.GRID) {
58+
val gridState = rememberLazyGridState()
3759

38-
LazyVerticalGridScrollbar(state = gridState) {
39-
LazyVerticalGrid(state = gridState, columns = GridCells.Adaptive(164.dp)) {
40-
item(span = { GridItemSpan(maxLineSpan) }) {
41-
PlaylistHeader(playlistInfo = it, totalDuration = totalDuration)
60+
LazyVerticalGridScrollbar(state = gridState) {
61+
LazyVerticalGrid(state = gridState, columns = GridCells.Adaptive(250.dp)) {
62+
item(span = { GridItemSpan(maxLineSpan) }) {
63+
PlaylistHeader(playlistInfo = it, totalDuration = totalDuration)
64+
}
65+
66+
items(streams.itemCount) {
67+
StreamGridItem(streams[it]!!, onClick)
68+
}
4269
}
70+
}
71+
} else {
72+
// Card or list views
73+
val listState = rememberLazyListState()
74+
75+
LazyColumnScrollbar(state = listState) {
76+
LazyColumn(state = listState) {
77+
item {
78+
PlaylistHeader(playlistInfo = it, totalDuration = totalDuration)
79+
}
4380

44-
items(streams.itemCount) {
45-
StreamGridItem(streams[it]!!)
81+
items(streams.itemCount) {
82+
val stream = streams[it]!!
83+
if (mode == ItemViewMode.CARD) {
84+
StreamCardItem(stream, onClick)
85+
} else {
86+
StreamListItem(stream, onClick)
87+
}
88+
}
4689
}
4790
}
4891
}

app/src/main/java/org/schabi/newpipe/compose/playlist/PlaylistHeader.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.schabi.newpipe.compose.playlist
22

33
import android.content.res.Configuration
4+
import androidx.compose.foundation.BorderStroke
45
import androidx.compose.foundation.Image
6+
import androidx.compose.foundation.border
57
import androidx.compose.foundation.clickable
68
import androidx.compose.foundation.layout.Arrangement
79
import androidx.compose.foundation.layout.Column
@@ -22,6 +24,7 @@ import androidx.compose.runtime.setValue
2224
import androidx.compose.ui.Alignment
2325
import androidx.compose.ui.Modifier
2426
import androidx.compose.ui.draw.clip
27+
import androidx.compose.ui.graphics.Color
2528
import androidx.compose.ui.platform.LocalContext
2629
import androidx.compose.ui.res.painterResource
2730
import androidx.compose.ui.res.stringResource
@@ -65,9 +68,9 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
6568
Row(
6669
verticalAlignment = Alignment.CenterVertically,
6770
horizontalArrangement = Arrangement.spacedBy(4.dp),
68-
modifier = Modifier.apply {
71+
modifier = Modifier.let {
6972
if (playlistInfo.uploaderName != null && playlistInfo.uploaderUrl != null) {
70-
clickable {
73+
it.clickable {
7174
try {
7275
NavigationHelper.openChannelFragment(
7376
(context as FragmentActivity).supportFragmentManager,
@@ -78,11 +81,13 @@ fun PlaylistHeader(playlistInfo: PlaylistInfo, totalDuration: Long) {
7881
ErrorUtil.showUiErrorSnackbar(context, "Opening channel fragment", e)
7982
}
8083
}
81-
}
84+
} else it
8285
}
8386
) {
8487
val imageModifier = Modifier
8588
.size(24.dp)
89+
.border(BorderStroke(1.dp, Color.White), CircleShape)
90+
.padding(1.dp)
8691
.clip(CircleShape)
8792
val isMix = YoutubeParsingHelper.isYoutubeMixId(playlistInfo.id) ||
8893
YoutubeParsingHelper.isYoutubeMusicMixId(playlistInfo.id)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.schabi.newpipe.compose.stream
2+
3+
import android.content.res.Configuration
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.MaterialTheme
11+
import androidx.compose.material3.Surface
12+
import androidx.compose.material3.Text
13+
import androidx.compose.runtime.Composable
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.layout.ContentScale
16+
import androidx.compose.ui.text.style.TextOverflow
17+
import androidx.compose.ui.tooling.preview.Preview
18+
import androidx.compose.ui.tooling.preview.PreviewParameter
19+
import androidx.compose.ui.unit.dp
20+
import org.schabi.newpipe.compose.theme.AppTheme
21+
import org.schabi.newpipe.extractor.stream.StreamInfoItem
22+
23+
@Composable
24+
fun StreamCardItem(stream: StreamInfoItem, onClick: (StreamInfoItem) -> Unit) {
25+
Column(
26+
modifier = Modifier
27+
.clickable(onClick = { onClick(stream) })
28+
.padding(top = 12.dp, start = 2.dp, end = 2.dp)
29+
) {
30+
StreamThumbnail(
31+
stream = stream,
32+
modifier = Modifier.fillMaxWidth(),
33+
contentScale = ContentScale.FillWidth
34+
)
35+
36+
Column(modifier = Modifier.padding(10.dp)) {
37+
Text(
38+
text = stream.name,
39+
overflow = TextOverflow.Ellipsis,
40+
style = MaterialTheme.typography.titleSmall,
41+
maxLines = 2
42+
)
43+
44+
Row(
45+
modifier = Modifier.fillMaxWidth(),
46+
horizontalArrangement = Arrangement.SpaceBetween
47+
) {
48+
Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
49+
50+
Text(
51+
text = getStreamInfoDetail(stream),
52+
style = MaterialTheme.typography.bodySmall
53+
)
54+
}
55+
}
56+
}
57+
}
58+
59+
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
60+
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
61+
@Composable
62+
private fun StreamCardItemPreview(
63+
@PreviewParameter(StreamItemPreviewProvider::class) stream: StreamInfoItem
64+
) {
65+
AppTheme {
66+
Surface(color = MaterialTheme.colorScheme.background) {
67+
StreamCardItem(stream) {}
68+
}
69+
}
70+
}
Lines changed: 8 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.schabi.newpipe.compose.stream
22

3-
import android.content.Context
43
import android.content.res.Configuration
54
import androidx.compose.foundation.clickable
65
import androidx.compose.foundation.layout.Column
@@ -11,47 +10,21 @@ import androidx.compose.material3.Surface
1110
import androidx.compose.material3.Text
1211
import androidx.compose.runtime.Composable
1312
import androidx.compose.ui.Modifier
14-
import androidx.compose.ui.platform.LocalContext
15-
import androidx.compose.ui.res.painterResource
1613
import androidx.compose.ui.text.style.TextOverflow
1714
import androidx.compose.ui.tooling.preview.Preview
1815
import androidx.compose.ui.tooling.preview.PreviewParameter
19-
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
2016
import androidx.compose.ui.unit.dp
21-
import androidx.fragment.app.FragmentActivity
22-
import coil.compose.AsyncImage
23-
import org.schabi.newpipe.R
2417
import org.schabi.newpipe.compose.theme.AppTheme
25-
import org.schabi.newpipe.extractor.Image
2618
import org.schabi.newpipe.extractor.stream.StreamInfoItem
27-
import org.schabi.newpipe.extractor.stream.StreamType
28-
import org.schabi.newpipe.util.Localization
29-
import org.schabi.newpipe.util.NO_SERVICE_ID
30-
import org.schabi.newpipe.util.NavigationHelper
31-
import org.schabi.newpipe.util.image.ImageStrategy
32-
import java.util.concurrent.TimeUnit
3319

3420
@Composable
35-
fun StreamGridItem(stream: StreamInfoItem) {
36-
val context = LocalContext.current
37-
21+
fun StreamGridItem(stream: StreamInfoItem, onClick: (StreamInfoItem) -> Unit) {
3822
Column(
3923
modifier = Modifier
40-
.clickable {
41-
NavigationHelper.openVideoDetailFragment(
42-
context, (context as FragmentActivity).supportFragmentManager,
43-
stream.serviceId, stream.url, stream.name, null, false
44-
)
45-
}
24+
.clickable(onClick = { onClick(stream) })
4625
.padding(12.dp)
4726
) {
48-
AsyncImage(
49-
model = ImageStrategy.choosePreferredImage(stream.thumbnails),
50-
contentDescription = null,
51-
placeholder = painterResource(R.drawable.placeholder_thumbnail_video),
52-
error = painterResource(R.drawable.placeholder_thumbnail_video),
53-
modifier = Modifier.size(width = 164.dp, height = 92.dp)
54-
)
27+
StreamThumbnail(stream = stream, modifier = Modifier.size(width = 246.dp, height = 138.dp))
5528

5629
Text(
5730
text = stream.name,
@@ -62,60 +35,13 @@ fun StreamGridItem(stream: StreamInfoItem) {
6235

6336
Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
6437

65-
Text(text = getStreamInfoDetail(context, stream), style = MaterialTheme.typography.bodySmall)
66-
}
67-
}
68-
69-
private fun getStreamInfoDetail(context: Context, stream: StreamInfoItem): String {
70-
val views = if (stream.viewCount >= 0) {
71-
when (stream.streamType) {
72-
StreamType.AUDIO_LIVE_STREAM -> Localization.listeningCount(context, stream.viewCount)
73-
StreamType.LIVE_STREAM -> Localization.shortWatchingCount(context, stream.viewCount)
74-
else -> Localization.shortViewCount(context, stream.viewCount)
75-
}
76-
} else {
77-
""
78-
}
79-
val date =
80-
Localization.relativeTimeOrTextual(context, stream.uploadDate, stream.textualUploadDate)
81-
82-
return if (views.isEmpty()) {
83-
date
84-
} else if (date.isNullOrEmpty()) {
85-
views
86-
} else {
87-
"$views$date"
38+
Text(
39+
text = getStreamInfoDetail(stream),
40+
style = MaterialTheme.typography.bodySmall
41+
)
8842
}
8943
}
9044

91-
fun StreamInfoItem(
92-
serviceId: Int = NO_SERVICE_ID,
93-
url: String = "",
94-
name: String = "Stream",
95-
streamType: StreamType,
96-
uploaderName: String? = "Uploader",
97-
uploaderUrl: String? = null,
98-
uploaderAvatars: List<Image> = emptyList(),
99-
duration: Long = TimeUnit.HOURS.toSeconds(1),
100-
viewCount: Long = 10,
101-
textualUploadDate: String = "1 month ago"
102-
) = StreamInfoItem(serviceId, url, name, streamType).apply {
103-
this.uploaderName = uploaderName
104-
this.uploaderUrl = uploaderUrl
105-
this.uploaderAvatars = uploaderAvatars
106-
this.duration = duration
107-
this.viewCount = viewCount
108-
this.textualUploadDate = textualUploadDate
109-
}
110-
111-
private class StreamItemPreviewProvider : PreviewParameterProvider<StreamInfoItem> {
112-
override val values = sequenceOf(
113-
StreamInfoItem(streamType = StreamType.NONE),
114-
StreamInfoItem(streamType = StreamType.LIVE_STREAM),
115-
StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM),
116-
)
117-
}
118-
11945
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
12046
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
12147
@Composable
@@ -124,7 +50,7 @@ private fun StreamGridItemPreview(
12450
) {
12551
AppTheme {
12652
Surface(color = MaterialTheme.colorScheme.background) {
127-
StreamGridItem(stream)
53+
StreamGridItem(stream, onClick = {})
12854
}
12955
}
13056
}

0 commit comments

Comments
 (0)