Access editor from long press menu + fix scrolling
This commit is contained in:
parent
a3af6e20ce
commit
0cc63347af
@ -5,6 +5,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.positionChange
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
|
||||
/**
|
||||
@ -15,31 +16,31 @@ import androidx.compose.ui.unit.IntOffset
|
||||
* [androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress].
|
||||
*
|
||||
* @param beginDragGesture called when the user first touches the screen (down event) with the
|
||||
* pointer position, should return `true` if the receiver wants to handle this gesture, `false`
|
||||
* otherwise.
|
||||
* @param handleDragGestureChange called with the current pointer position, every time the user
|
||||
* moves the finger after [beginDragGesture] has returned `true`.
|
||||
* @param endDragGesture called when the drag gesture finishes after [beginDragGesture] has returned
|
||||
* `true`.
|
||||
* pointer position.
|
||||
* @param handleDragGestureChange called with the current pointer position and the difference from
|
||||
* the last position, every time the user moves the finger after [beginDragGesture] has been called.
|
||||
* @param endDragGesture called when the drag gesture finishes, after [beginDragGesture] has been
|
||||
* called.
|
||||
*/
|
||||
fun Modifier.detectDragGestures(
|
||||
beginDragGesture: (IntOffset) -> Boolean,
|
||||
handleDragGestureChange: (IntOffset) -> Unit,
|
||||
beginDragGesture: (position: IntOffset) -> Unit,
|
||||
handleDragGestureChange: (position: IntOffset, positionChange: Offset) -> Unit,
|
||||
endDragGesture: () -> Unit
|
||||
): Modifier {
|
||||
return this.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown()
|
||||
val pointerId = down.id
|
||||
if (!beginDragGesture(down.position.toIntOffset())) {
|
||||
return@awaitEachGesture
|
||||
}
|
||||
beginDragGesture(down.position.toIntOffset())
|
||||
while (true) {
|
||||
val change = awaitPointerEvent().changes.find { it.id == pointerId }
|
||||
if (change == null || !change.pressed) {
|
||||
break
|
||||
}
|
||||
handleDragGestureChange(change.position.toIntOffset())
|
||||
handleDragGestureChange(
|
||||
change.position.toIntOffset(),
|
||||
change.positionChange(),
|
||||
)
|
||||
change.consume()
|
||||
}
|
||||
endDragGesture()
|
||||
|
||||
@ -7,6 +7,7 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -37,16 +38,15 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -73,6 +73,7 @@ import coil3.compose.AsyncImage
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.ktx.popFirst
|
||||
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
|
||||
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.EnqueueNext
|
||||
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.ShowChannelDetails
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
@ -119,100 +120,127 @@ fun LongPressMenu(
|
||||
longPressable: LongPressable,
|
||||
longPressActions: List<LongPressAction>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onEditActions: () -> Unit = {}, // TODO handle this menu
|
||||
sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
dragHandle = { LongPressMenuDragHandle(onEditActions) },
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 6.dp, end = 6.dp, bottom = 16.dp)
|
||||
var showEditor by rememberSaveable(key = longPressable.url) { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
if (showEditor) {
|
||||
// we can't put the editor in a bottom sheet, because it relies on dragging gestures
|
||||
ScaffoldWithToolbar(
|
||||
title = stringResource(R.string.long_press_menu_actions_editor),
|
||||
onBackClick = { showEditor = false },
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
LongPressMenuEditor()
|
||||
}
|
||||
BackHandler { showEditor = false }
|
||||
}
|
||||
} else {
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = onDismissRequest,
|
||||
dragHandle = { LongPressMenuDragHandle(onEditActions = { showEditor = true }) },
|
||||
) {
|
||||
val buttonHeight = MinButtonWidth // landscape aspect ratio, square in the limit
|
||||
val headerWidthInButtons = 5 // the header is 5 times as wide as the buttons
|
||||
val buttonsPerRow = (this.maxWidth / MinButtonWidth).toInt()
|
||||
LongPressMenuContent(
|
||||
longPressable = longPressable,
|
||||
longPressActions = longPressActions,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the channel icon goes in the menu header, so do not show a button for it
|
||||
val actions = longPressActions.toMutableList()
|
||||
val ctx = LocalContext.current
|
||||
val onUploaderClick = actions.popFirst { it.type == ShowChannelDetails }
|
||||
?.let { showChannelDetailsAction ->
|
||||
{
|
||||
showChannelDetailsAction.action(ctx)
|
||||
onDismissRequest()
|
||||
}
|
||||
@Composable
|
||||
private fun LongPressMenuContent(
|
||||
longPressable: LongPressable,
|
||||
longPressActions: List<LongPressAction>,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 6.dp, end = 6.dp, bottom = 16.dp)
|
||||
) {
|
||||
val buttonHeight = MinButtonWidth // landscape aspect ratio, square in the limit
|
||||
val headerWidthInButtons = 5 // the header is 5 times as wide as the buttons
|
||||
val buttonsPerRow = (this.maxWidth / MinButtonWidth).toInt()
|
||||
|
||||
// the channel icon goes in the menu header, so do not show a button for it
|
||||
val actions = longPressActions.toMutableList()
|
||||
val ctx = LocalContext.current
|
||||
val onUploaderClick = actions.popFirst { it.type == ShowChannelDetails }
|
||||
?.let { showChannelDetailsAction ->
|
||||
{
|
||||
showChannelDetailsAction.action(ctx)
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
var actionIndex = -1 // -1 indicates the header
|
||||
while (actionIndex < actions.size) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
var rowIndex = 0
|
||||
while (rowIndex < buttonsPerRow) {
|
||||
if (actionIndex >= actions.size) {
|
||||
// no more buttons to show, fill the rest of the row with a
|
||||
// spacer that has the same weight as the missing buttons, so that
|
||||
// the other buttons don't grow too wide
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(buttonHeight)
|
||||
.fillMaxWidth()
|
||||
.weight((buttonsPerRow - rowIndex).toFloat()),
|
||||
)
|
||||
break
|
||||
} else if (actionIndex >= 0) {
|
||||
val action = actions[actionIndex]
|
||||
LongPressMenuButton(
|
||||
icon = action.type.icon,
|
||||
text = stringResource(action.type.label),
|
||||
onClick = {
|
||||
action.action(ctx)
|
||||
onDismissRequest()
|
||||
},
|
||||
enabled = action.enabled(false),
|
||||
modifier = Modifier
|
||||
.height(buttonHeight)
|
||||
.fillMaxWidth()
|
||||
.weight(1F),
|
||||
)
|
||||
rowIndex += 1
|
||||
} else if (headerWidthInButtons >= buttonsPerRow) {
|
||||
// this branch is taken if the header is going to fit on one line
|
||||
// (i.e. on phones in portrait)
|
||||
LongPressMenuHeader(
|
||||
item = longPressable,
|
||||
onUploaderClick = onUploaderClick,
|
||||
modifier = Modifier
|
||||
// leave the height as small as possible, since it's the
|
||||
// only item on the row anyway
|
||||
.padding(start = 6.dp, end = 6.dp, bottom = 6.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(headerWidthInButtons.toFloat()),
|
||||
)
|
||||
rowIndex += headerWidthInButtons
|
||||
} else {
|
||||
// this branch is taken if the header will have some buttons to its
|
||||
// right (i.e. on tablets or on phones in landscape)
|
||||
LongPressMenuHeader(
|
||||
item = longPressable,
|
||||
onUploaderClick = onUploaderClick,
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.heightIn(min = 70.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(headerWidthInButtons.toFloat()),
|
||||
)
|
||||
rowIndex += headerWidthInButtons
|
||||
}
|
||||
actionIndex += 1
|
||||
Column {
|
||||
var actionIndex = -1 // -1 indicates the header
|
||||
while (actionIndex < actions.size) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
var rowIndex = 0
|
||||
while (rowIndex < buttonsPerRow) {
|
||||
if (actionIndex >= actions.size) {
|
||||
// no more buttons to show, fill the rest of the row with a
|
||||
// spacer that has the same weight as the missing buttons, so that
|
||||
// the other buttons don't grow too wide
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(buttonHeight)
|
||||
.fillMaxWidth()
|
||||
.weight((buttonsPerRow - rowIndex).toFloat()),
|
||||
)
|
||||
break
|
||||
} else if (actionIndex >= 0) {
|
||||
val action = actions[actionIndex]
|
||||
LongPressMenuButton(
|
||||
icon = action.type.icon,
|
||||
text = stringResource(action.type.label),
|
||||
onClick = {
|
||||
action.action(ctx)
|
||||
onDismissRequest()
|
||||
},
|
||||
enabled = action.enabled(false),
|
||||
modifier = Modifier
|
||||
.height(buttonHeight)
|
||||
.fillMaxWidth()
|
||||
.weight(1F),
|
||||
)
|
||||
rowIndex += 1
|
||||
} else if (headerWidthInButtons >= buttonsPerRow) {
|
||||
// this branch is taken if the header is going to fit on one line
|
||||
// (i.e. on phones in portrait)
|
||||
LongPressMenuHeader(
|
||||
item = longPressable,
|
||||
onUploaderClick = onUploaderClick,
|
||||
modifier = Modifier
|
||||
// leave the height as small as possible, since it's the
|
||||
// only item on the row anyway
|
||||
.padding(start = 6.dp, end = 6.dp, bottom = 6.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(headerWidthInButtons.toFloat()),
|
||||
)
|
||||
rowIndex += headerWidthInButtons
|
||||
} else {
|
||||
// this branch is taken if the header will have some buttons to its
|
||||
// right (i.e. on tablets or on phones in landscape)
|
||||
LongPressMenuHeader(
|
||||
item = longPressable,
|
||||
onUploaderClick = onUploaderClick,
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.heightIn(min = 70.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(headerWidthInButtons.toFloat()),
|
||||
)
|
||||
rowIndex += headerWidthInButtons
|
||||
}
|
||||
actionIndex += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -619,14 +647,12 @@ private fun LongPressMenuPreview(
|
||||
AppTheme(useDarkTheme = useDarkTheme) {
|
||||
// longPressable is null when running the preview in an emulator for some reason...
|
||||
@Suppress("USELESS_ELVIS")
|
||||
LongPressMenu(
|
||||
LongPressMenuContent(
|
||||
longPressable = longPressable ?: LongPressablePreviews().values.first(),
|
||||
onDismissRequest = {},
|
||||
longPressActions = LongPressAction.Type.entries
|
||||
// disable Enqueue actions just to show it off
|
||||
.map { t -> t.buildAction({ t != EnqueueNext }) { } },
|
||||
onEditActions = { useDarkTheme = !useDarkTheme },
|
||||
sheetState = rememberStandardBottomSheetState(), // makes it start out as open
|
||||
onDismissRequest = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,9 @@ package org.schabi.newpipe.ui.components.menu
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
@ -50,6 +52,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -69,6 +72,7 @@ import org.schabi.newpipe.util.text.FixedHeightCenteredText
|
||||
|
||||
private const val ItemNotFound = -1
|
||||
|
||||
// TODO implement accessibility for this, to allow using this with a DPAD (e.g. Android TV)
|
||||
@Composable
|
||||
fun LongPressMenuEditor() {
|
||||
// We get the current arrangement once and do not observe on purpose
|
||||
@ -135,8 +139,8 @@ fun LongPressMenuEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
fun beginDragGesture(pos: IntOffset): Boolean {
|
||||
val item = findItemForOffsetOrClosestInRow(pos) ?: return false
|
||||
fun beginDragGesture(pos: IntOffset) {
|
||||
val item = findItemForOffsetOrClosestInRow(pos) ?: return
|
||||
val i = item.index
|
||||
val enabledActionIndex = indexOfEnabledAction(i)
|
||||
val hiddenActionIndex = indexOfHiddenAction(i)
|
||||
@ -149,15 +153,18 @@ fun LongPressMenuEditor() {
|
||||
activeDragAction = hiddenActions[hiddenActionIndex]
|
||||
hiddenActions[hiddenActionIndex] = ActionOrMarker.DragMarker
|
||||
} else {
|
||||
return false
|
||||
return
|
||||
}
|
||||
activeDragPosition = pos
|
||||
activeDragSize = item.size
|
||||
return true
|
||||
}
|
||||
|
||||
fun handleDragGestureChange(pos: IntOffset) {
|
||||
if (activeDragAction == null) return
|
||||
fun handleDragGestureChange(pos: IntOffset, posChange: Offset) {
|
||||
if (activeDragAction == null) {
|
||||
// when the user clicks outside of any draggable item, let the list be scrolled
|
||||
gridState.dispatchRawDelta(-posChange.y)
|
||||
return
|
||||
}
|
||||
activeDragPosition = pos
|
||||
val item = findItemForOffsetOrClosestInRow(pos) ?: return
|
||||
val i = item.index
|
||||
@ -223,6 +230,7 @@ fun LongPressMenuEditor() {
|
||||
),
|
||||
// same width as the LongPressMenu
|
||||
columns = GridCells.Adaptive(MinButtonWidth),
|
||||
userScrollEnabled = false,
|
||||
state = gridState,
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
@ -267,6 +275,11 @@ fun LongPressMenuEditor() {
|
||||
itemsIndexed(hiddenActions, key = { _, action -> action.stableUniqueKey() }) { _, action ->
|
||||
ActionOrMarkerUi(modifier = Modifier.animateItem(), action = action)
|
||||
}
|
||||
item {
|
||||
// make the grid size a bit bigger to let items be dragged at the bottom and to give
|
||||
// the view some space to resizing without jumping up and down
|
||||
Spacer(modifier = Modifier.height(MinButtonWidth))
|
||||
}
|
||||
}
|
||||
if (activeDragAction != null) {
|
||||
val size = with(LocalDensity.current) {
|
||||
|
||||
@ -909,4 +909,5 @@
|
||||
<string name="long_press_menu_hidden_actions_description">Drag the header or the actions to this section to hide them</string>
|
||||
<string name="header">Header</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="long_press_menu_actions_editor">Reorder and disable actions</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user