Skip to content

Commit 5699233

Browse files
committed
Start implementing LongPressMenu
Implement content preview
1 parent 8f3c8ac commit 5699233

2 files changed

Lines changed: 273 additions & 0 deletions

File tree

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
@file:OptIn(ExperimentalMaterial3Api::class)
2+
3+
package org.schabi.newpipe.ui.components.menu
4+
5+
import androidx.compose.foundation.basicMarquee
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.Row
10+
import androidx.compose.foundation.layout.Spacer
11+
import androidx.compose.foundation.layout.fillMaxHeight
12+
import androidx.compose.foundation.layout.fillMaxWidth
13+
import androidx.compose.foundation.layout.height
14+
import androidx.compose.foundation.layout.padding
15+
import androidx.compose.foundation.layout.width
16+
import androidx.compose.foundation.layout.widthIn
17+
import androidx.compose.material.icons.Icons
18+
import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
19+
import androidx.compose.material3.ExperimentalMaterial3Api
20+
import androidx.compose.material3.Icon
21+
import androidx.compose.material3.MaterialTheme
22+
import androidx.compose.material3.ModalBottomSheet
23+
import androidx.compose.material3.SheetState
24+
import androidx.compose.material3.Surface
25+
import androidx.compose.material3.Text
26+
import androidx.compose.material3.rememberModalBottomSheetState
27+
import androidx.compose.material3.rememberStandardBottomSheetState
28+
import androidx.compose.runtime.Composable
29+
import androidx.compose.ui.Alignment
30+
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.draw.clip
32+
import androidx.compose.ui.graphics.Color
33+
import androidx.compose.ui.platform.LocalContext
34+
import androidx.compose.ui.res.painterResource
35+
import androidx.compose.ui.tooling.preview.Preview
36+
import androidx.compose.ui.tooling.preview.PreviewParameter
37+
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
38+
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
39+
import androidx.compose.ui.unit.dp
40+
import coil3.compose.AsyncImage
41+
import org.schabi.newpipe.R
42+
import org.schabi.newpipe.player.playqueue.PlayQueue
43+
import org.schabi.newpipe.player.playqueue.SinglePlayQueue
44+
import org.schabi.newpipe.util.Either
45+
import org.schabi.newpipe.util.Localization
46+
import java.time.OffsetDateTime
47+
48+
@Composable
49+
fun LongPressMenu(
50+
longPressable: LongPressable,
51+
onDismissRequest: () -> Unit,
52+
sheetState: SheetState = rememberModalBottomSheetState(),
53+
) {
54+
ModalBottomSheet(
55+
onDismissRequest,
56+
sheetState = sheetState,
57+
) {
58+
Column {
59+
LongPressMenuHeader(
60+
item = longPressable,
61+
modifier = Modifier
62+
.padding(horizontal = 12.dp)
63+
.fillMaxWidth()
64+
)
65+
Spacer(Modifier.height(100.dp))
66+
}
67+
}
68+
}
69+
70+
@Composable
71+
fun LongPressMenuHeader(item: LongPressable, modifier: Modifier = Modifier) {
72+
val ctx = LocalContext.current
73+
74+
Surface(
75+
color = MaterialTheme.colorScheme.primaryContainer,
76+
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
77+
shape = MaterialTheme.shapes.large,
78+
modifier = modifier,
79+
) {
80+
Row {
81+
Box(
82+
modifier = Modifier.height(70.dp)
83+
) {
84+
if (item.thumbnailUrl != null) {
85+
AsyncImage(
86+
model = item.thumbnailUrl,
87+
contentDescription = null,
88+
placeholder = painterResource(R.drawable.placeholder_thumbnail_video),
89+
error = painterResource(R.drawable.placeholder_thumbnail_video),
90+
modifier = Modifier
91+
.fillMaxHeight()
92+
.widthIn(max = 125.dp) // 16:9 thumbnail at most
93+
.clip(MaterialTheme.shapes.large)
94+
)
95+
}
96+
97+
item.playlistSize?.let { playlistSize ->
98+
Surface(
99+
color = Color.Black.copy(alpha = 0.6f),
100+
contentColor = Color.White,
101+
modifier = Modifier
102+
.align(Alignment.TopEnd)
103+
.fillMaxHeight()
104+
.width(40.dp)
105+
.clip(MaterialTheme.shapes.large),
106+
) {
107+
Column(
108+
verticalArrangement = Arrangement.Center,
109+
horizontalAlignment = Alignment.CenterHorizontally,
110+
modifier = Modifier.fillMaxWidth(),
111+
) {
112+
Icon(
113+
Icons.AutoMirrored.Default.PlaylistPlay,
114+
contentDescription = null,
115+
)
116+
Text(
117+
text = Localization.localizeStreamCountMini(ctx, playlistSize),
118+
style = MaterialTheme.typography.labelMedium,
119+
maxLines = 1,
120+
)
121+
}
122+
}
123+
}
124+
125+
item.duration?.takeIf { it >= 0 }?.let { duration ->
126+
// only show duration if there is a thumbnail and there is no playlist header
127+
if (item.thumbnailUrl != null && item.playlistSize == null) {
128+
Surface(
129+
color = Color.Black.copy(alpha = 0.6f),
130+
contentColor = Color.White,
131+
modifier = Modifier
132+
.align(Alignment.BottomEnd)
133+
.padding(4.dp)
134+
.clip(MaterialTheme.shapes.medium),
135+
) {
136+
Text(
137+
text = Localization.getDurationString(duration),
138+
modifier = Modifier.padding(vertical = 2.dp, horizontal = 3.dp)
139+
)
140+
}
141+
}
142+
}
143+
}
144+
145+
Column(
146+
verticalArrangement = Arrangement.SpaceBetween,
147+
modifier = Modifier
148+
.height(70.dp)
149+
.padding(vertical = 12.dp, horizontal = 12.dp),
150+
) {
151+
Text(
152+
text = item.title,
153+
style = MaterialTheme.typography.titleMedium,
154+
maxLines = 1,
155+
modifier = Modifier.basicMarquee(iterations = Int.MAX_VALUE),
156+
)
157+
158+
Text(
159+
text = Localization.concatenateStrings(
160+
item.uploader,
161+
item.uploadDate?.match<String>(
162+
{ it },
163+
{ Localization.localizeUploadDate(ctx, it) }
164+
),
165+
item.viewCount?.let { Localization.localizeViewCount(ctx, it) }
166+
),
167+
style = MaterialTheme.typography.bodyMedium,
168+
modifier = Modifier.basicMarquee(iterations = Int.MAX_VALUE),
169+
)
170+
}
171+
}
172+
}
173+
}
174+
175+
private class LongPressablePreviews : CollectionPreviewParameterProvider<LongPressable>(
176+
listOf(
177+
object : LongPressable {
178+
override val title: String = "Big Buck Bunny"
179+
override val url: String = "https://www.youtube.com/watch?v=YE7VzlLtp-4"
180+
override val thumbnailUrl: String =
181+
"https://i.ytimg.com/vi_webp/YE7VzlLtp-4/maxresdefault.webp"
182+
override val uploader: String = "Blender"
183+
override val uploaderUrl: String = "https://www.youtube.com/@BlenderOfficial"
184+
override val viewCount: Long = 8765432
185+
override val uploadDate: Either<String, OffsetDateTime> = Either.left("16 years ago")
186+
override val playlistSize: Long = 12
187+
override val duration: Long = 500
188+
189+
override fun getPlayQueue(): PlayQueue {
190+
return SinglePlayQueue(listOf(), 0)
191+
}
192+
},
193+
object : LongPressable {
194+
override val title: String = LoremIpsum().values.first()
195+
override val url: String = "https://www.youtube.com/watch?v=YE7VzlLtp-4"
196+
override val thumbnailUrl: String? = null
197+
override val uploader: String = "Blender"
198+
override val uploaderUrl: String = "https://www.youtube.com/@BlenderOfficial"
199+
override val viewCount: Long = 8765432
200+
override val uploadDate: Either<String, OffsetDateTime> = Either.left("16 years ago")
201+
override val playlistSize: Long? = null
202+
override val duration: Long = 500
203+
204+
override fun getPlayQueue(): PlayQueue {
205+
return SinglePlayQueue(listOf(), 0)
206+
}
207+
},
208+
object : LongPressable {
209+
override val title: String = LoremIpsum().values.first()
210+
override val url: String = "https://www.youtube.com/watch?v=YE7VzlLtp-4"
211+
override val thumbnailUrl: String =
212+
"https://i.ytimg.com/vi_webp/YE7VzlLtp-4/maxresdefault.webp"
213+
override val uploader: String? = null
214+
override val uploaderUrl: String? = null
215+
override val viewCount: Long? = null
216+
override val uploadDate: Either<String, OffsetDateTime>? = null
217+
override val playlistSize: Long? = null
218+
override val duration: Long = 500
219+
220+
override fun getPlayQueue(): PlayQueue {
221+
return SinglePlayQueue(listOf(), 0)
222+
}
223+
},
224+
object : LongPressable {
225+
override val title: String = LoremIpsum().values.first()
226+
override val url: String = "https://www.youtube.com/watch?v=YE7VzlLtp-4"
227+
override val thumbnailUrl: String? = null
228+
override val uploader: String? = null
229+
override val uploaderUrl: String? = null
230+
override val viewCount: Long? = null
231+
override val uploadDate: Either<String, OffsetDateTime>? = null
232+
override val playlistSize: Long = 1500
233+
override val duration: Long = 500
234+
235+
override fun getPlayQueue(): PlayQueue {
236+
return SinglePlayQueue(listOf(), 0)
237+
}
238+
}
239+
)
240+
)
241+
242+
@Preview
243+
@Composable
244+
private fun LongPressMenuPreview(
245+
@PreviewParameter(LongPressablePreviews::class) longPressable: LongPressable
246+
) {
247+
LongPressMenu(
248+
longPressable = longPressable,
249+
onDismissRequest = {},
250+
sheetState = rememberStandardBottomSheetState(), // makes it start out as open
251+
)
252+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.schabi.newpipe.ui.components.menu
2+
3+
import androidx.compose.runtime.Stable
4+
import org.schabi.newpipe.player.playqueue.PlayQueue
5+
import org.schabi.newpipe.util.Either
6+
import java.time.OffsetDateTime
7+
8+
@Stable
9+
interface LongPressable {
10+
val title: String
11+
val url: String?
12+
val thumbnailUrl: String?
13+
val uploader: String?
14+
val uploaderUrl: String?
15+
val viewCount: Long?
16+
val uploadDate: Either<String, OffsetDateTime>?
17+
val playlistSize: Long? // null if this is not a playlist
18+
val duration: Long?
19+
20+
fun getPlayQueue(): PlayQueue
21+
}

0 commit comments

Comments
 (0)