1- @file:OptIn(ExperimentalMaterial3Api ::class , ExperimentalLayoutApi :: class )
1+ @file:OptIn(ExperimentalMaterial3Api ::class )
22
33package org.schabi.newpipe.ui.components.menu
44
@@ -12,16 +12,14 @@ import androidx.compose.foundation.layout.Arrangement
1212import androidx.compose.foundation.layout.Box
1313import androidx.compose.foundation.layout.BoxWithConstraints
1414import androidx.compose.foundation.layout.Column
15- import androidx.compose.foundation.layout.ExperimentalLayoutApi
16- import androidx.compose.foundation.layout.FlowRow
1715import androidx.compose.foundation.layout.PaddingValues
1816import androidx.compose.foundation.layout.Row
1917import androidx.compose.foundation.layout.Spacer
2018import androidx.compose.foundation.layout.fillMaxWidth
2119import androidx.compose.foundation.layout.height
20+ import androidx.compose.foundation.layout.heightIn
2221import androidx.compose.foundation.layout.padding
2322import androidx.compose.foundation.layout.size
24- import androidx.compose.foundation.layout.width
2523import androidx.compose.foundation.layout.widthIn
2624import androidx.compose.material.icons.Icons
2725import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
@@ -63,12 +61,13 @@ import androidx.compose.ui.tooling.preview.Preview
6361import androidx.compose.ui.tooling.preview.PreviewParameter
6462import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
6563import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
66- import androidx.compose.ui.unit.Dp
6764import androidx.compose.ui.unit.dp
68- import androidx.compose.ui.unit.times
6965import coil3.compose.AsyncImage
7066import org.schabi.newpipe.R
7167import org.schabi.newpipe.extractor.stream.StreamInfoItem
68+ import org.schabi.newpipe.ktx.popFirst
69+ import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.EnqueueNext
70+ import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.ShowChannelDetails
7271import org.schabi.newpipe.ui.theme.AppTheme
7372import org.schabi.newpipe.ui.theme.customColors
7473import org.schabi.newpipe.util.Either
@@ -97,7 +96,7 @@ fun getLongPressMenuView(
9796 },
9897 ),
9998 onDismissRequest = { (this .parent as ViewGroup ).removeView(this ) },
100- actions = LongPressAction .buildActionList(item, false ),
99+ longPressActions = LongPressAction .buildActionList(item, false ),
101100 onEditActions = {},
102101 )
103102 }
@@ -108,7 +107,7 @@ fun getLongPressMenuView(
108107fun LongPressMenu (
109108 longPressable : LongPressable ,
110109 onDismissRequest : () -> Unit ,
111- actions : List <LongPressAction >,
110+ longPressActions : List <LongPressAction >,
112111 onEditActions : () -> Unit ,
113112 sheetState : SheetState = rememberModalBottomSheetState(),
114113) {
@@ -120,58 +119,86 @@ fun LongPressMenu(
120119 BoxWithConstraints (
121120 modifier = Modifier
122121 .fillMaxWidth()
123- .padding(bottom = 16 .dp)
122+ .padding(start = 6 .dp, end = 6 .dp, bottom = 16 .dp)
124123 ) {
125- val maxContainerWidth = maxWidth
126- val minButtonWidth = 70 .dp
127- val buttonHeight = 70 .dp
128- val padding = 12 .dp
129- val boxCount = ((maxContainerWidth - padding) / (minButtonWidth + padding)).toInt()
130- val buttonWidth = (maxContainerWidth - (boxCount + 1 ) * padding) / boxCount
131- val desiredHeaderWidth = buttonWidth * 4 + padding * 3
124+ val minButtonWidth = 80 .dp
125+ val buttonHeight = 85 .dp
126+ val headerWidthInButtons = 5 // the header is 5 times as wide as the buttons
127+ val buttonsPerRow = (maxWidth / minButtonWidth).toInt()
132128
133- FlowRow (
134- horizontalArrangement = Arrangement .spacedBy(padding),
135- verticalArrangement = Arrangement .spacedBy(padding),
136- // left and right padding are implicit in the .align(Center), this way approximation
137- // errors in the calculations above don't make the items wrap at the wrong position
138- modifier = Modifier .align(Alignment .Center ),
139- ) {
140- val actionsWithoutChannel = actions.toMutableList()
141- val showChannelAction = actionsWithoutChannel.indexOfFirst {
142- it.type == LongPressAction .Type .ShowChannelDetails
143- }.let { i ->
144- if (i >= 0 ) {
145- actionsWithoutChannel.removeAt(i)
146- } else {
147- null
148- }
149- }
129+ // the channel icon goes in the menu header, so do not show a button for it
130+ val actions = longPressActions.toMutableList()
131+ val showChannelAction = actions.popFirst { it.type == ShowChannelDetails }
132+ val ctx = LocalContext .current
150133
151- LongPressMenuHeader (
152- item = longPressable,
153- thumbnailHeight = buttonHeight,
154- onUploaderClickAction = showChannelAction?.action,
155- // subtract 2.dp to account for approximation errors in the calculations above
156- modifier = if (desiredHeaderWidth >= maxContainerWidth - 2 * padding - 2 .dp) {
157- // leave the height as small as possible, since it's the only item on the
158- // row anyway
159- Modifier .width(maxContainerWidth - 2 * padding)
160- } else {
161- // make sure it has the same height as other buttons
162- Modifier .size(desiredHeaderWidth, buttonHeight)
163- }
164- )
134+ Column {
135+ var actionIndex = - 1 // -1 indicates the header
136+ while (actionIndex < actions.size) {
137+ Row (
138+ verticalAlignment = Alignment .CenterVertically ,
139+ modifier = Modifier .fillMaxWidth(),
140+ ) {
141+ var rowIndex = 0
142+ while (rowIndex < buttonsPerRow) {
143+ if (actionIndex >= actions.size) {
144+ // no more buttons to show, fill the rest of the row with a
145+ // spacer that has the same weight as the missing buttons, so that
146+ // the other buttons don't grow too wide
147+ Spacer (
148+ modifier = Modifier
149+ .height(buttonHeight)
150+ .fillMaxWidth()
151+ .weight((buttonsPerRow - rowIndex).toFloat()),
152+ )
153+ break
165154
166- val ctx = LocalContext .current
167- for (action in actionsWithoutChannel) {
168- LongPressMenuButton (
169- icon = action.type.icon,
170- text = stringResource(action.type.label),
171- onClick = { action.action(ctx) },
172- enabled = action.enabled(false ),
173- modifier = Modifier .size(buttonWidth, buttonHeight),
174- )
155+ } else if (actionIndex >= 0 ) {
156+ val action = actions[actionIndex]
157+ LongPressMenuButton (
158+ icon = action.type.icon,
159+ text = stringResource(action.type.label),
160+ onClick = { action.action(ctx) },
161+ enabled = action.enabled(false ),
162+ modifier = Modifier
163+ .height(buttonHeight)
164+ .fillMaxWidth()
165+ .weight(1F ),
166+ )
167+ rowIndex + = 1
168+
169+ } else if (headerWidthInButtons >= buttonsPerRow) {
170+ // this branch is taken if the header is going to fit on one line
171+ // (i.e. on phones in portrait)
172+ LongPressMenuHeader (
173+ item = longPressable,
174+ onUploaderClickAction = showChannelAction?.action,
175+ modifier = Modifier
176+ // leave the height as small as possible, since it's the
177+ // only item on the row anyway
178+ .padding(start = 6 .dp, end = 6 .dp, bottom = 6 .dp)
179+ .fillMaxWidth()
180+ .weight(headerWidthInButtons.toFloat()),
181+ )
182+ rowIndex + = headerWidthInButtons
183+
184+ } else {
185+ // this branch is taken if the header will have some buttons to its
186+ // right (i.e. on tablets or on phones in landscape)
187+ LongPressMenuHeader (
188+ item = longPressable,
189+ onUploaderClickAction = showChannelAction?.action,
190+ modifier = Modifier
191+ .padding(6 .dp)
192+ .heightIn(min = 70 .dp)
193+ .fillMaxWidth()
194+ .weight(headerWidthInButtons.toFloat()),
195+ )
196+ rowIndex + = headerWidthInButtons
197+
198+ }
199+ actionIndex + = 1
200+ }
201+ }
175202 }
176203 }
177204 }
@@ -209,7 +236,6 @@ fun LongPressMenuDragHandle(onEditActions: () -> Unit = {}) {
209236@Composable
210237fun LongPressMenuHeader (
211238 item : LongPressable ,
212- thumbnailHeight : Dp ,
213239 onUploaderClickAction : ((context: Context ) -> Unit )? ,
214240 modifier : Modifier = Modifier ,
215241) {
@@ -230,7 +256,7 @@ fun LongPressMenuHeader(
230256 placeholder = painterResource(R .drawable.placeholder_thumbnail_video),
231257 error = painterResource(R .drawable.placeholder_thumbnail_video),
232258 modifier = Modifier
233- .height(thumbnailHeight )
259+ .height(70 .dp )
234260 .widthIn(max = 125 .dp) // 16:9 thumbnail at most
235261 .clip(MaterialTheme .shapes.large)
236262 )
@@ -280,8 +306,7 @@ fun LongPressMenuHeader(
280306 contentColor = Color .White ,
281307 modifier = Modifier
282308 .align(Alignment .TopEnd )
283- .height(thumbnailHeight)
284- .width(40 .dp)
309+ .size(width = 40 .dp, height = 70 .dp)
285310 .clip(MaterialTheme .shapes.large),
286311 ) {
287312 Column (
@@ -403,7 +428,7 @@ fun LongPressMenuButton(
403428 onClick = onClick,
404429 enabled = enabled,
405430 shape = MaterialTheme .shapes.large,
406- contentPadding = PaddingValues (4 .dp),
431+ contentPadding = PaddingValues (start = 3 .dp, top = 8 .dp, end = 3 .dp, bottom = 2 .dp),
407432 border = null ,
408433 modifier = modifier,
409434 ) {
@@ -510,9 +535,9 @@ private fun LongPressMenuPreview(
510535 LongPressMenu (
511536 longPressable = longPressable ? : LongPressablePreviews ().values.first(),
512537 onDismissRequest = {},
513- actions = LongPressAction .Type .entries
538+ longPressActions = LongPressAction .Type .entries
514539 // disable Enqueue actions just to show it off
515- .map { t -> t.buildAction({ ! t.name.startsWith( " E " ) }) { } },
540+ .map { t -> t.buildAction({ t != EnqueueNext }) { } },
516541 onEditActions = { useDarkTheme = ! useDarkTheme },
517542 sheetState = rememberStandardBottomSheetState(), // makes it start out as open
518543 )
0 commit comments