Skip to content

Commit 771551c

Browse files
committed
Access editor from long press menu + fix scrolling
1 parent 813a4f5 commit 771551c

4 files changed

Lines changed: 153 additions & 112 deletions

File tree

app/src/main/java/org/schabi/newpipe/ui/DetectDragModifier.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown
55
import androidx.compose.ui.Modifier
66
import androidx.compose.ui.geometry.Offset
77
import androidx.compose.ui.input.pointer.pointerInput
8+
import androidx.compose.ui.input.pointer.positionChange
89
import androidx.compose.ui.unit.IntOffset
910

1011
/**
@@ -15,31 +16,31 @@ import androidx.compose.ui.unit.IntOffset
1516
* [androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress].
1617
*
1718
* @param beginDragGesture called when the user first touches the screen (down event) with the
18-
* pointer position, should return `true` if the receiver wants to handle this gesture, `false`
19-
* otherwise.
20-
* @param handleDragGestureChange called with the current pointer position, every time the user
21-
* moves the finger after [beginDragGesture] has returned `true`.
22-
* @param endDragGesture called when the drag gesture finishes after [beginDragGesture] has returned
23-
* `true`.
19+
* pointer position.
20+
* @param handleDragGestureChange called with the current pointer position and the difference from
21+
* the last position, every time the user moves the finger after [beginDragGesture] has been called.
22+
* @param endDragGesture called when the drag gesture finishes, after [beginDragGesture] has been
23+
* called.
2424
*/
2525
fun Modifier.detectDragGestures(
26-
beginDragGesture: (IntOffset) -> Boolean,
27-
handleDragGestureChange: (IntOffset) -> Unit,
26+
beginDragGesture: (position: IntOffset) -> Unit,
27+
handleDragGestureChange: (position: IntOffset, positionChange: Offset) -> Unit,
2828
endDragGesture: () -> Unit
2929
): Modifier {
3030
return this.pointerInput(Unit) {
3131
awaitEachGesture {
3232
val down = awaitFirstDown()
3333
val pointerId = down.id
34-
if (!beginDragGesture(down.position.toIntOffset())) {
35-
return@awaitEachGesture
36-
}
34+
beginDragGesture(down.position.toIntOffset())
3735
while (true) {
3836
val change = awaitPointerEvent().changes.find { it.id == pointerId }
3937
if (change == null || !change.pressed) {
4038
break
4139
}
42-
handleDragGestureChange(change.position.toIntOffset())
40+
handleDragGestureChange(
41+
change.position.toIntOffset(),
42+
change.positionChange(),
43+
)
4344
change.consume()
4445
}
4546
endDragGesture()

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

Lines changed: 120 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.content.Context
77
import android.content.res.Configuration
88
import android.view.ViewGroup
99
import android.view.ViewGroup.LayoutParams
10+
import androidx.activity.compose.BackHandler
1011
import androidx.compose.foundation.clickable
1112
import androidx.compose.foundation.isSystemInDarkTheme
1213
import androidx.compose.foundation.layout.Arrangement
@@ -37,16 +38,15 @@ import androidx.compose.material3.IconButton
3738
import androidx.compose.material3.MaterialTheme
3839
import androidx.compose.material3.ModalBottomSheet
3940
import androidx.compose.material3.OutlinedButton
40-
import androidx.compose.material3.SheetState
4141
import androidx.compose.material3.Surface
4242
import androidx.compose.material3.Text
4343
import androidx.compose.material3.rememberModalBottomSheetState
44-
import androidx.compose.material3.rememberStandardBottomSheetState
4544
import androidx.compose.runtime.Composable
4645
import androidx.compose.runtime.DisposableEffect
4746
import androidx.compose.runtime.getValue
4847
import androidx.compose.runtime.mutableStateOf
4948
import androidx.compose.runtime.remember
49+
import androidx.compose.runtime.saveable.rememberSaveable
5050
import androidx.compose.runtime.setValue
5151
import androidx.compose.ui.Alignment
5252
import androidx.compose.ui.Modifier
@@ -73,6 +73,7 @@ import coil3.compose.AsyncImage
7373
import org.schabi.newpipe.R
7474
import org.schabi.newpipe.extractor.stream.StreamType
7575
import org.schabi.newpipe.ktx.popFirst
76+
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
7677
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.EnqueueNext
7778
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.ShowChannelDetails
7879
import org.schabi.newpipe.ui.theme.AppTheme
@@ -119,100 +120,127 @@ fun LongPressMenu(
119120
longPressable: LongPressable,
120121
longPressActions: List<LongPressAction>,
121122
onDismissRequest: () -> Unit,
122-
onEditActions: () -> Unit = {}, // TODO handle this menu
123-
sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
124123
) {
125-
ModalBottomSheet(
126-
onDismissRequest,
127-
sheetState = sheetState,
128-
dragHandle = { LongPressMenuDragHandle(onEditActions) },
129-
) {
130-
BoxWithConstraints(
131-
modifier = Modifier
132-
.fillMaxWidth()
133-
.padding(start = 6.dp, end = 6.dp, bottom = 16.dp)
124+
var showEditor by rememberSaveable(key = longPressable.url) { mutableStateOf(false) }
125+
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
126+
127+
if (showEditor) {
128+
// we can't put the editor in a bottom sheet, because it relies on dragging gestures
129+
ScaffoldWithToolbar(
130+
title = stringResource(R.string.long_press_menu_actions_editor),
131+
onBackClick = { showEditor = false },
132+
) { paddingValues ->
133+
Box(modifier = Modifier.padding(paddingValues)) {
134+
LongPressMenuEditor()
135+
}
136+
BackHandler { showEditor = false }
137+
}
138+
} else {
139+
ModalBottomSheet(
140+
sheetState = sheetState,
141+
onDismissRequest = onDismissRequest,
142+
dragHandle = { LongPressMenuDragHandle(onEditActions = { showEditor = true }) },
134143
) {
135-
val buttonHeight = MinButtonWidth // landscape aspect ratio, square in the limit
136-
val headerWidthInButtons = 5 // the header is 5 times as wide as the buttons
137-
val buttonsPerRow = (this.maxWidth / MinButtonWidth).toInt()
144+
LongPressMenuContent(
145+
longPressable = longPressable,
146+
longPressActions = longPressActions,
147+
onDismissRequest = onDismissRequest,
148+
)
149+
}
150+
}
151+
}
138152

139-
// the channel icon goes in the menu header, so do not show a button for it
140-
val actions = longPressActions.toMutableList()
141-
val ctx = LocalContext.current
142-
val onUploaderClick = actions.popFirst { it.type == ShowChannelDetails }
143-
?.let { showChannelDetailsAction ->
144-
{
145-
showChannelDetailsAction.action(ctx)
146-
onDismissRequest()
147-
}
153+
@Composable
154+
private fun LongPressMenuContent(
155+
longPressable: LongPressable,
156+
longPressActions: List<LongPressAction>,
157+
onDismissRequest: () -> Unit,
158+
) {
159+
BoxWithConstraints(
160+
modifier = Modifier
161+
.fillMaxWidth()
162+
.padding(start = 6.dp, end = 6.dp, bottom = 16.dp)
163+
) {
164+
val buttonHeight = MinButtonWidth // landscape aspect ratio, square in the limit
165+
val headerWidthInButtons = 5 // the header is 5 times as wide as the buttons
166+
val buttonsPerRow = (this.maxWidth / MinButtonWidth).toInt()
167+
168+
// the channel icon goes in the menu header, so do not show a button for it
169+
val actions = longPressActions.toMutableList()
170+
val ctx = LocalContext.current
171+
val onUploaderClick = actions.popFirst { it.type == ShowChannelDetails }
172+
?.let { showChannelDetailsAction ->
173+
{
174+
showChannelDetailsAction.action(ctx)
175+
onDismissRequest()
148176
}
177+
}
149178

150-
Column {
151-
var actionIndex = -1 // -1 indicates the header
152-
while (actionIndex < actions.size) {
153-
Row(
154-
verticalAlignment = Alignment.CenterVertically,
155-
modifier = Modifier.fillMaxWidth(),
156-
) {
157-
var rowIndex = 0
158-
while (rowIndex < buttonsPerRow) {
159-
if (actionIndex >= actions.size) {
160-
// no more buttons to show, fill the rest of the row with a
161-
// spacer that has the same weight as the missing buttons, so that
162-
// the other buttons don't grow too wide
163-
Spacer(
164-
modifier = Modifier
165-
.height(buttonHeight)
166-
.fillMaxWidth()
167-
.weight((buttonsPerRow - rowIndex).toFloat()),
168-
)
169-
break
170-
} else if (actionIndex >= 0) {
171-
val action = actions[actionIndex]
172-
LongPressMenuButton(
173-
icon = action.type.icon,
174-
text = stringResource(action.type.label),
175-
onClick = {
176-
action.action(ctx)
177-
onDismissRequest()
178-
},
179-
enabled = action.enabled(false),
180-
modifier = Modifier
181-
.height(buttonHeight)
182-
.fillMaxWidth()
183-
.weight(1F),
184-
)
185-
rowIndex += 1
186-
} else if (headerWidthInButtons >= buttonsPerRow) {
187-
// this branch is taken if the header is going to fit on one line
188-
// (i.e. on phones in portrait)
189-
LongPressMenuHeader(
190-
item = longPressable,
191-
onUploaderClick = onUploaderClick,
192-
modifier = Modifier
193-
// leave the height as small as possible, since it's the
194-
// only item on the row anyway
195-
.padding(start = 6.dp, end = 6.dp, bottom = 6.dp)
196-
.fillMaxWidth()
197-
.weight(headerWidthInButtons.toFloat()),
198-
)
199-
rowIndex += headerWidthInButtons
200-
} else {
201-
// this branch is taken if the header will have some buttons to its
202-
// right (i.e. on tablets or on phones in landscape)
203-
LongPressMenuHeader(
204-
item = longPressable,
205-
onUploaderClick = onUploaderClick,
206-
modifier = Modifier
207-
.padding(6.dp)
208-
.heightIn(min = 70.dp)
209-
.fillMaxWidth()
210-
.weight(headerWidthInButtons.toFloat()),
211-
)
212-
rowIndex += headerWidthInButtons
213-
}
214-
actionIndex += 1
179+
Column {
180+
var actionIndex = -1 // -1 indicates the header
181+
while (actionIndex < actions.size) {
182+
Row(
183+
verticalAlignment = Alignment.CenterVertically,
184+
modifier = Modifier.fillMaxWidth(),
185+
) {
186+
var rowIndex = 0
187+
while (rowIndex < buttonsPerRow) {
188+
if (actionIndex >= actions.size) {
189+
// no more buttons to show, fill the rest of the row with a
190+
// spacer that has the same weight as the missing buttons, so that
191+
// the other buttons don't grow too wide
192+
Spacer(
193+
modifier = Modifier
194+
.height(buttonHeight)
195+
.fillMaxWidth()
196+
.weight((buttonsPerRow - rowIndex).toFloat()),
197+
)
198+
break
199+
} else if (actionIndex >= 0) {
200+
val action = actions[actionIndex]
201+
LongPressMenuButton(
202+
icon = action.type.icon,
203+
text = stringResource(action.type.label),
204+
onClick = {
205+
action.action(ctx)
206+
onDismissRequest()
207+
},
208+
enabled = action.enabled(false),
209+
modifier = Modifier
210+
.height(buttonHeight)
211+
.fillMaxWidth()
212+
.weight(1F),
213+
)
214+
rowIndex += 1
215+
} else if (headerWidthInButtons >= buttonsPerRow) {
216+
// this branch is taken if the header is going to fit on one line
217+
// (i.e. on phones in portrait)
218+
LongPressMenuHeader(
219+
item = longPressable,
220+
onUploaderClick = onUploaderClick,
221+
modifier = Modifier
222+
// leave the height as small as possible, since it's the
223+
// only item on the row anyway
224+
.padding(start = 6.dp, end = 6.dp, bottom = 6.dp)
225+
.fillMaxWidth()
226+
.weight(headerWidthInButtons.toFloat()),
227+
)
228+
rowIndex += headerWidthInButtons
229+
} else {
230+
// this branch is taken if the header will have some buttons to its
231+
// right (i.e. on tablets or on phones in landscape)
232+
LongPressMenuHeader(
233+
item = longPressable,
234+
onUploaderClick = onUploaderClick,
235+
modifier = Modifier
236+
.padding(6.dp)
237+
.heightIn(min = 70.dp)
238+
.fillMaxWidth()
239+
.weight(headerWidthInButtons.toFloat()),
240+
)
241+
rowIndex += headerWidthInButtons
215242
}
243+
actionIndex += 1
216244
}
217245
}
218246
}
@@ -619,14 +647,12 @@ private fun LongPressMenuPreview(
619647
AppTheme(useDarkTheme = useDarkTheme) {
620648
// longPressable is null when running the preview in an emulator for some reason...
621649
@Suppress("USELESS_ELVIS")
622-
LongPressMenu(
650+
LongPressMenuContent(
623651
longPressable = longPressable ?: LongPressablePreviews().values.first(),
624-
onDismissRequest = {},
625652
longPressActions = LongPressAction.Type.entries
626653
// disable Enqueue actions just to show it off
627654
.map { t -> t.buildAction({ t != EnqueueNext }) { } },
628-
onEditActions = { useDarkTheme = !useDarkTheme },
629-
sheetState = rememberStandardBottomSheetState(), // makes it start out as open
655+
onDismissRequest = {},
630656
)
631657
}
632658
}

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ package org.schabi.newpipe.ui.components.menu
2020

2121
import androidx.annotation.StringRes
2222
import androidx.compose.foundation.layout.Column
23+
import androidx.compose.foundation.layout.Spacer
2324
import androidx.compose.foundation.layout.fillMaxWidth
25+
import androidx.compose.foundation.layout.height
2426
import androidx.compose.foundation.layout.offset
2527
import androidx.compose.foundation.layout.padding
2628
import androidx.compose.foundation.layout.safeDrawingPadding
@@ -50,6 +52,7 @@ import androidx.compose.runtime.setValue
5052
import androidx.compose.runtime.toMutableStateList
5153
import androidx.compose.ui.Alignment
5254
import androidx.compose.ui.Modifier
55+
import androidx.compose.ui.geometry.Offset
5356
import androidx.compose.ui.graphics.Color
5457
import androidx.compose.ui.platform.LocalDensity
5558
import androidx.compose.ui.res.stringResource
@@ -69,6 +72,7 @@ import org.schabi.newpipe.util.text.FixedHeightCenteredText
6972

7073
private const val ItemNotFound = -1
7174

75+
// TODO implement accessibility for this, to allow using this with a DPAD (e.g. Android TV)
7276
@Composable
7377
fun LongPressMenuEditor() {
7478
// We get the current arrangement once and do not observe on purpose
@@ -135,8 +139,8 @@ fun LongPressMenuEditor() {
135139
}
136140
}
137141

138-
fun beginDragGesture(pos: IntOffset): Boolean {
139-
val item = findItemForOffsetOrClosestInRow(pos) ?: return false
142+
fun beginDragGesture(pos: IntOffset) {
143+
val item = findItemForOffsetOrClosestInRow(pos) ?: return
140144
val i = item.index
141145
val enabledActionIndex = indexOfEnabledAction(i)
142146
val hiddenActionIndex = indexOfHiddenAction(i)
@@ -149,15 +153,18 @@ fun LongPressMenuEditor() {
149153
activeDragAction = hiddenActions[hiddenActionIndex]
150154
hiddenActions[hiddenActionIndex] = ActionOrMarker.DragMarker
151155
} else {
152-
return false
156+
return
153157
}
154158
activeDragPosition = pos
155159
activeDragSize = item.size
156-
return true
157160
}
158161

159-
fun handleDragGestureChange(pos: IntOffset) {
160-
if (activeDragAction == null) return
162+
fun handleDragGestureChange(pos: IntOffset, posChange: Offset) {
163+
if (activeDragAction == null) {
164+
// when the user clicks outside of any draggable item, let the list be scrolled
165+
gridState.dispatchRawDelta(-posChange.y)
166+
return
167+
}
161168
activeDragPosition = pos
162169
val item = findItemForOffsetOrClosestInRow(pos) ?: return
163170
val i = item.index
@@ -223,6 +230,7 @@ fun LongPressMenuEditor() {
223230
),
224231
// same width as the LongPressMenu
225232
columns = GridCells.Adaptive(MinButtonWidth),
233+
userScrollEnabled = false,
226234
state = gridState,
227235
) {
228236
item(span = { GridItemSpan(maxLineSpan) }) {
@@ -267,6 +275,11 @@ fun LongPressMenuEditor() {
267275
itemsIndexed(hiddenActions, key = { _, action -> action.stableUniqueKey() }) { _, action ->
268276
ActionOrMarkerUi(modifier = Modifier.animateItem(), action = action)
269277
}
278+
item {
279+
// make the grid size a bit bigger to let items be dragged at the bottom and to give
280+
// the view some space to resizing without jumping up and down
281+
Spacer(modifier = Modifier.height(MinButtonWidth))
282+
}
270283
}
271284
if (activeDragAction != null) {
272285
val size = with(LocalDensity.current) {

0 commit comments

Comments
 (0)