1- @file:OptIn(ExperimentalMaterial3Api ::class )
1+ @file:OptIn(ExperimentalMaterial3Api ::class , ExperimentalLayoutApi :: class )
22
33package org.schabi.newpipe.ui.components.menu
44
55import androidx.compose.foundation.basicMarquee
66import androidx.compose.foundation.layout.Arrangement
77import androidx.compose.foundation.layout.Box
8+ import androidx.compose.foundation.layout.BoxWithConstraints
89import androidx.compose.foundation.layout.Column
10+ import androidx.compose.foundation.layout.ExperimentalLayoutApi
11+ import androidx.compose.foundation.layout.FlowRow
912import androidx.compose.foundation.layout.Row
1013import androidx.compose.foundation.layout.Spacer
11- import androidx.compose.foundation.layout.fillMaxHeight
1214import androidx.compose.foundation.layout.fillMaxWidth
1315import androidx.compose.foundation.layout.height
1416import androidx.compose.foundation.layout.padding
17+ import androidx.compose.foundation.layout.size
1518import androidx.compose.foundation.layout.width
1619import androidx.compose.foundation.layout.widthIn
1720import androidx.compose.material.icons.Icons
@@ -36,7 +39,9 @@ import androidx.compose.ui.tooling.preview.Preview
3639import androidx.compose.ui.tooling.preview.PreviewParameter
3740import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
3841import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
42+ import androidx.compose.ui.unit.Dp
3943import androidx.compose.ui.unit.dp
44+ import androidx.compose.ui.unit.times
4045import coil3.compose.AsyncImage
4146import org.schabi.newpipe.R
4247import org.schabi.newpipe.player.playqueue.PlayQueue
@@ -55,20 +60,53 @@ fun LongPressMenu(
5560 onDismissRequest,
5661 sheetState = sheetState,
5762 ) {
58- Column {
59- LongPressMenuHeader (
60- item = longPressable,
61- modifier = Modifier
62- .padding(horizontal = 12 .dp)
63- .fillMaxWidth()
64- )
65- Spacer (Modifier .height(100 .dp))
63+ BoxWithConstraints (
64+ modifier = Modifier .fillMaxWidth()
65+ .padding(bottom = 16 .dp)
66+ ) {
67+ val maxContainerWidth = maxWidth
68+ val minButtonWidth = 60 .dp
69+ val buttonHeight = 70 .dp
70+ val padding = 12 .dp
71+ val boxCount = ((maxContainerWidth - padding) / (minButtonWidth + padding)).toInt()
72+ val buttonWidth = (maxContainerWidth - (boxCount + 1 ) * padding) / boxCount
73+ val desiredHeaderWidth = buttonWidth * 5 + padding * 4
74+
75+ FlowRow (
76+ horizontalArrangement = Arrangement .spacedBy(padding),
77+ verticalArrangement = Arrangement .spacedBy(padding),
78+ // left and right padding are implicit in the .align(Center), this way approximation
79+ // errors in the calculations above don't make the items wrap at the wrong position
80+ modifier = Modifier .align(Alignment .Center ),
81+ ) {
82+ LongPressMenuHeader (
83+ item = longPressable,
84+ thumbnailHeight = buttonHeight,
85+ // subtract 2.dp to account for approximation errors in the calculations above
86+ modifier = if (desiredHeaderWidth >= maxContainerWidth - 2 * padding - 2 .dp) {
87+ // leave the height as small as possible, since it's the only item on the
88+ // row anyway
89+ Modifier .width(maxContainerWidth - 2 * padding)
90+ } else {
91+ // make sure it has the same height as other buttons
92+ Modifier .size(desiredHeaderWidth, buttonHeight)
93+ }
94+ )
95+
96+ for (i in 0 .. 10 ) {
97+ LongPressMenuButton (modifier = Modifier .size(buttonWidth, buttonHeight))
98+ }
99+ }
66100 }
67101 }
68102}
69103
70104@Composable
71- fun LongPressMenuHeader (item : LongPressable , modifier : Modifier = Modifier ) {
105+ fun LongPressMenuHeader (
106+ item : LongPressable ,
107+ thumbnailHeight : Dp ,
108+ modifier : Modifier = Modifier ,
109+ ) {
72110 val ctx = LocalContext .current
73111
74112 Surface (
@@ -77,18 +115,16 @@ fun LongPressMenuHeader(item: LongPressable, modifier: Modifier = Modifier) {
77115 shape = MaterialTheme .shapes.large,
78116 modifier = modifier,
79117 ) {
80- Row {
81- Box (
82- modifier = Modifier .height(70 .dp)
83- ) {
118+ Row (verticalAlignment = Alignment .CenterVertically ) {
119+ Box {
84120 if (item.thumbnailUrl != null ) {
85121 AsyncImage (
86122 model = item.thumbnailUrl,
87123 contentDescription = null ,
88124 placeholder = painterResource(R .drawable.placeholder_thumbnail_video),
89125 error = painterResource(R .drawable.placeholder_thumbnail_video),
90126 modifier = Modifier
91- .fillMaxHeight( )
127+ .height(thumbnailHeight )
92128 .widthIn(max = 125 .dp) // 16:9 thumbnail at most
93129 .clip(MaterialTheme .shapes.large)
94130 )
@@ -100,7 +136,7 @@ fun LongPressMenuHeader(item: LongPressable, modifier: Modifier = Modifier) {
100136 contentColor = Color .White ,
101137 modifier = Modifier
102138 .align(Alignment .TopEnd )
103- .fillMaxHeight( )
139+ .height(thumbnailHeight )
104140 .width(40 .dp)
105141 .clip(MaterialTheme .shapes.large),
106142 ) {
@@ -143,10 +179,7 @@ fun LongPressMenuHeader(item: LongPressable, modifier: Modifier = Modifier) {
143179 }
144180
145181 Column (
146- verticalArrangement = Arrangement .SpaceBetween ,
147- modifier = Modifier
148- .height(70 .dp)
149- .padding(vertical = 12 .dp, horizontal = 12 .dp),
182+ modifier = Modifier .padding(vertical = 12 .dp, horizontal = 12 .dp),
150183 ) {
151184 Text (
152185 text = item.title,
@@ -155,23 +188,36 @@ fun LongPressMenuHeader(item: LongPressable, modifier: Modifier = Modifier) {
155188 modifier = Modifier .basicMarquee(iterations = Int .MAX_VALUE ),
156189 )
157190
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) }
191+ val subtitle = Localization .concatenateStrings(
192+ item.uploader,
193+ item.uploadDate?.match<String >(
194+ { it },
195+ { Localization .relativeTime(it) }
166196 ),
167- style = MaterialTheme .typography.bodyMedium,
168- modifier = Modifier .basicMarquee(iterations = Int .MAX_VALUE ),
197+ item.viewCount?.let { Localization .localizeViewCount(ctx, it) }
169198 )
199+ if (subtitle.isNotBlank()) {
200+ Spacer (Modifier .height(1 .dp))
201+ Text (
202+ text = subtitle,
203+ style = MaterialTheme .typography.bodyMedium,
204+ modifier = Modifier .basicMarquee(iterations = Int .MAX_VALUE ),
205+ )
206+ }
170207 }
171208 }
172209 }
173210}
174211
212+ @Composable
213+ fun LongPressMenuButton (modifier : Modifier = Modifier ) {
214+ Surface (
215+ color = Color .Black ,
216+ modifier = modifier,
217+ shape = MaterialTheme .shapes.large,
218+ ) { }
219+ }
220+
175221private class LongPressablePreviews : CollectionPreviewParameterProvider <LongPressable >(
176222 listOf(
177223 object : LongPressable {
@@ -205,6 +251,21 @@ private class LongPressablePreviews : CollectionPreviewParameterProvider<LongPre
205251 return SinglePlayQueue (listOf(), 0)
206252 }
207253 },
254+ object : LongPressable {
255+ override val title: String = LoremIpsum ().values.first()
256+ override val url: String = "https://www.youtube.com/watch? v=YE7VzlLtp -4"
257+ override val thumbnailUrl: String? = null
258+ override val uploader: String? = null
259+ override val uploaderUrl: String = "https://www.youtube.com/@BlenderOfficial "
260+ override val viewCount: Long? = null
261+ override val uploadDate: Either <String , OffsetDateTime >? = null
262+ override val playlistSize: Long? = null
263+ override val duration: Long? = null
264+
265+ override fun getPlayQueue(): PlayQueue {
266+ return SinglePlayQueue (listOf(), 0)
267+ }
268+ },
208269 object : LongPressable {
209270 override val title: String = LoremIpsum ().values.first()
210271 override val url: String = "https://www.youtube.com/watch? v=YE7VzlLtp -4"
@@ -228,7 +289,7 @@ private class LongPressablePreviews : CollectionPreviewParameterProvider<LongPre
228289 override val uploader: String? = null
229290 override val uploaderUrl: String? = null
230291 override val viewCount: Long? = null
231- override val uploadDate: Either <String , OffsetDateTime >? = null
292+ override val uploadDate: Either <String , OffsetDateTime > = Either .right( OffsetDateTime .now().minusSeconds(12))
232293 override val playlistSize: Long = 1500
233294 override val duration: Long = 500
234295
@@ -240,13 +301,14 @@ private class LongPressablePreviews : CollectionPreviewParameterProvider<LongPre
240301)
241302
242303@Preview
304+ @Preview(device = " spec:width=1280dp,height=800dp,dpi=240" )
243305@Composable
244306private fun LongPressMenuPreview (
245307 @PreviewParameter(LongPressablePreviews ::class ) longPressable : LongPressable
246308) {
247309 LongPressMenu (
248- longPressable = longPressable ,
310+ longPressable = LongPressablePreviews ().values.toList()[ 4 ] ,
249311 onDismissRequest = {},
250- sheetState = rememberStandardBottomSheetState(), // makes it start out as open
312+ sheetState = rememberStandardBottomSheetState(), // makes it start out as open
251313 )
252314}
0 commit comments