Skip to content

Commit d5cc530

Browse files
committed
Improve how long press menu buttons are laid out
1 parent 8e9da3f commit d5cc530

File tree

2 files changed

+97
-63
lines changed

2 files changed

+97
-63
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.schabi.newpipe.ktx
2+
3+
fun <A> MutableList<A>.popFirst(filter: (A) -> Boolean): A? {
4+
val i = indexOfFirst(filter)
5+
if (i < 0) {
6+
return null
7+
}
8+
return removeAt(i)
9+
}

app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt

Lines changed: 88 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
1+
@file:OptIn(ExperimentalMaterial3Api::class)
22

33
package org.schabi.newpipe.ui.components.menu
44

@@ -12,16 +12,14 @@ import androidx.compose.foundation.layout.Arrangement
1212
import androidx.compose.foundation.layout.Box
1313
import androidx.compose.foundation.layout.BoxWithConstraints
1414
import androidx.compose.foundation.layout.Column
15-
import androidx.compose.foundation.layout.ExperimentalLayoutApi
16-
import androidx.compose.foundation.layout.FlowRow
1715
import androidx.compose.foundation.layout.PaddingValues
1816
import androidx.compose.foundation.layout.Row
1917
import androidx.compose.foundation.layout.Spacer
2018
import androidx.compose.foundation.layout.fillMaxWidth
2119
import androidx.compose.foundation.layout.height
20+
import androidx.compose.foundation.layout.heightIn
2221
import androidx.compose.foundation.layout.padding
2322
import androidx.compose.foundation.layout.size
24-
import androidx.compose.foundation.layout.width
2523
import androidx.compose.foundation.layout.widthIn
2624
import androidx.compose.material.icons.Icons
2725
import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
@@ -63,12 +61,13 @@ import androidx.compose.ui.tooling.preview.Preview
6361
import androidx.compose.ui.tooling.preview.PreviewParameter
6462
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
6563
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
66-
import androidx.compose.ui.unit.Dp
6764
import androidx.compose.ui.unit.dp
68-
import androidx.compose.ui.unit.times
6965
import coil3.compose.AsyncImage
7066
import org.schabi.newpipe.R
7167
import 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
7271
import org.schabi.newpipe.ui.theme.AppTheme
7372
import org.schabi.newpipe.ui.theme.customColors
7473
import 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(
108107
fun 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
210237
fun 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

Comments
 (0)